moved coretk under daemon/core/gui
|
@ -21,24 +21,3 @@ repos:
|
|||
language: system
|
||||
entry: bash -c 'cd daemon && pipenv run flake8'
|
||||
types: [python]
|
||||
|
||||
- id: isort-tk
|
||||
name: coretk-isort
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd coretk && pipenv run isort --atomic -y'
|
||||
types: [python]
|
||||
|
||||
- id: black-tk
|
||||
name: coretk-black
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd coretk && pipenv run black .'
|
||||
types: [python]
|
||||
|
||||
- id: flake8-tk
|
||||
name: coretk-flake8
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd coretk && pipenv run flake8'
|
||||
types: [python]
|
||||
|
|
|
@ -5,8 +5,10 @@ verify_ssl = true
|
|||
|
||||
[scripts]
|
||||
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf"
|
||||
coretk = "python core/gui/app.py"
|
||||
test = "pytest -v tests"
|
||||
test_emane = "pytest -v tests/emane"
|
||||
test-mock = "pytest -v --mock tests"
|
||||
test-emane = "pytest -v tests/emane"
|
||||
|
||||
[dev-packages]
|
||||
grpcio-tools = "*"
|
||||
|
|
577
daemon/Pipfile.lock
generated
|
@ -39,38 +39,41 @@
|
|||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:00d890313797d9fe4420506613384b43099ad7d2b905c0752dbcc3a6f14d80fa",
|
||||
"sha256:0cf9e550ac6c5e57b713437e2f4ac2d7fd0cd10336525a27224f5fc1ec2ee59a",
|
||||
"sha256:0ea23c9c0cdd6778146a50d867d6405693ac3b80a68829966c98dd5e1bbae400",
|
||||
"sha256:193697c2918ecdb3865acf6557cddf5076bb39f1f654975e087b67efdff83365",
|
||||
"sha256:1ae14b542bf3b35e5229439c35653d2ef7d8316c1fffb980f9b7647e544baa98",
|
||||
"sha256:1e389e069450609c6ffa37f21f40cce36f9be7643bbe5051ab1de99d5a779526",
|
||||
"sha256:263242b6ace7f9cd4ea401428d2d45066b49a700852334fd55311bde36dcda14",
|
||||
"sha256:33142ae9807665fa6511cfa9857132b2c3ee6ddffb012b3f0933fc11e1e830d5",
|
||||
"sha256:364f8404034ae1b232335d8c7f7b57deac566f148f7222cef78cf8ae28ef764e",
|
||||
"sha256:47368f69fe6529f8f49a5d146ddee713fc9057e31d61e8b6dc86a6a5e38cecc1",
|
||||
"sha256:4895640844f17bec32943995dc8c96989226974dfeb9dd121cc45d36e0d0c434",
|
||||
"sha256:558b3afef987cf4b17abd849e7bedf64ee12b28175d564d05b628a0f9355599b",
|
||||
"sha256:5ba86e1d80d458b338bda676fd9f9d68cb4e7a03819632969cf6d46b01a26730",
|
||||
"sha256:63424daa6955e6b4c70dc2755897f5be1d719eabe71b2625948b222775ed5c43",
|
||||
"sha256:6381a7d8b1ebd0bc27c3bc85bc1bfadbb6e6f756b4d4db0aa1425c3719ba26b4",
|
||||
"sha256:6381ab708158c4e1639da1f2a7679a9bbe3e5a776fc6d1fd808076f0e3145331",
|
||||
"sha256:6fd58366747debfa5e6163ada468a90788411f10c92597d3b0a912d07e580c36",
|
||||
"sha256:728ec653964655d65408949b07f9b2219df78badd601d6c49e28d604efe40599",
|
||||
"sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8",
|
||||
"sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8",
|
||||
"sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa",
|
||||
"sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78",
|
||||
"sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc",
|
||||
"sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e",
|
||||
"sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2",
|
||||
"sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0",
|
||||
"sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71",
|
||||
"sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891",
|
||||
"sha256:ec2fa3ee81707a5232bf2dfbd6623fdb278e070d596effc7e2d788f2ada71a05",
|
||||
"sha256:fd82eb4694be712fcae03c717ca2e0fc720657ac226b80bbb597e971fc6928c2"
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
],
|
||||
"version": "==1.13.1"
|
||||
"version": "==1.13.2"
|
||||
},
|
||||
"core": {
|
||||
"editable": true,
|
||||
|
@ -111,40 +114,51 @@
|
|||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115",
|
||||
"sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503",
|
||||
"sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff",
|
||||
"sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb",
|
||||
"sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7",
|
||||
"sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208",
|
||||
"sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55",
|
||||
"sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d",
|
||||
"sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408",
|
||||
"sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a",
|
||||
"sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1",
|
||||
"sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b",
|
||||
"sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f",
|
||||
"sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6",
|
||||
"sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b",
|
||||
"sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b",
|
||||
"sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6",
|
||||
"sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407",
|
||||
"sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b",
|
||||
"sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc",
|
||||
"sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950",
|
||||
"sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049",
|
||||
"sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900",
|
||||
"sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9",
|
||||
"sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695",
|
||||
"sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855",
|
||||
"sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4",
|
||||
"sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1",
|
||||
"sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6",
|
||||
"sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5",
|
||||
"sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22",
|
||||
"sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909"
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"invoke": {
|
||||
"hashes": [
|
||||
|
@ -156,58 +170,104 @@
|
|||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4",
|
||||
"sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc",
|
||||
"sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1",
|
||||
"sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046",
|
||||
"sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36",
|
||||
"sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5",
|
||||
"sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d",
|
||||
"sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916",
|
||||
"sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0",
|
||||
"sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27",
|
||||
"sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc",
|
||||
"sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7",
|
||||
"sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38",
|
||||
"sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5",
|
||||
"sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832",
|
||||
"sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a",
|
||||
"sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f",
|
||||
"sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9",
|
||||
"sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692",
|
||||
"sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84",
|
||||
"sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79",
|
||||
"sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681"
|
||||
"sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
|
||||
"sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
|
||||
"sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
|
||||
"sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
|
||||
"sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
|
||||
"sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
|
||||
"sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
|
||||
"sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
|
||||
"sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
|
||||
"sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
|
||||
"sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
|
||||
"sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
|
||||
"sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
|
||||
"sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
|
||||
"sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
|
||||
"sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
|
||||
"sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
|
||||
"sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
|
||||
"sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
|
||||
"sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
|
||||
"sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
|
||||
"sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
|
||||
"sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
|
||||
"sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
|
||||
"sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
|
||||
"sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
|
||||
],
|
||||
"version": "==4.4.1"
|
||||
"version": "==4.4.2"
|
||||
},
|
||||
"netaddr": {
|
||||
"hashes": [
|
||||
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
|
||||
"sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"
|
||||
],
|
||||
"version": "==0.7.19"
|
||||
},
|
||||
"paramiko": {
|
||||
"hashes": [
|
||||
"sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf",
|
||||
"sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041"
|
||||
"sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
|
||||
"sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
"version": "==2.7.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031",
|
||||
"sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71",
|
||||
"sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c",
|
||||
"sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340",
|
||||
"sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa",
|
||||
"sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b",
|
||||
"sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573",
|
||||
"sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e",
|
||||
"sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab",
|
||||
"sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9",
|
||||
"sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e",
|
||||
"sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291",
|
||||
"sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12",
|
||||
"sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871",
|
||||
"sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281",
|
||||
"sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08",
|
||||
"sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41",
|
||||
"sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2",
|
||||
"sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5",
|
||||
"sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb",
|
||||
"sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547",
|
||||
"sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75",
|
||||
"sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9",
|
||||
"sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1",
|
||||
"sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a",
|
||||
"sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96",
|
||||
"sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132",
|
||||
"sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a",
|
||||
"sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5",
|
||||
"sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0"
|
||||
],
|
||||
"version": "==6.2.1"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f",
|
||||
"sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77",
|
||||
"sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657",
|
||||
"sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896",
|
||||
"sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf",
|
||||
"sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6",
|
||||
"sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b",
|
||||
"sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300",
|
||||
"sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a",
|
||||
"sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789",
|
||||
"sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe",
|
||||
"sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc",
|
||||
"sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1",
|
||||
"sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe",
|
||||
"sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09",
|
||||
"sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de"
|
||||
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd",
|
||||
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed",
|
||||
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057",
|
||||
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce",
|
||||
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03",
|
||||
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46",
|
||||
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33",
|
||||
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c",
|
||||
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9",
|
||||
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef",
|
||||
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b",
|
||||
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d",
|
||||
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8",
|
||||
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6",
|
||||
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941",
|
||||
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13"
|
||||
],
|
||||
"version": "==3.10.0"
|
||||
"version": "==3.11.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
|
@ -241,12 +301,28 @@
|
|||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
],
|
||||
"version": "==5.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.13.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
|
@ -264,13 +340,6 @@
|
|||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
|
@ -309,101 +378,123 @@
|
|||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
|
||||
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.8"
|
||||
"version": "==3.7.9"
|
||||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115",
|
||||
"sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503",
|
||||
"sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff",
|
||||
"sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb",
|
||||
"sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7",
|
||||
"sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208",
|
||||
"sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55",
|
||||
"sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d",
|
||||
"sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408",
|
||||
"sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a",
|
||||
"sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1",
|
||||
"sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b",
|
||||
"sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f",
|
||||
"sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6",
|
||||
"sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b",
|
||||
"sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b",
|
||||
"sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6",
|
||||
"sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407",
|
||||
"sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b",
|
||||
"sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc",
|
||||
"sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950",
|
||||
"sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049",
|
||||
"sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900",
|
||||
"sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9",
|
||||
"sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695",
|
||||
"sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855",
|
||||
"sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4",
|
||||
"sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1",
|
||||
"sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6",
|
||||
"sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5",
|
||||
"sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22",
|
||||
"sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909"
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:0a849994d7d6411ca6147bb1db042b61ba6232eb5c90c69de5380a441bf80a75",
|
||||
"sha256:0db96ed52816471ceec8807aedf5cb4fd133ca201f614464cb46ca58584edf84",
|
||||
"sha256:1b98720459204e9afa33928e4fd53aeec6598afb7f704ed497f6926c67f12b9b",
|
||||
"sha256:200479310cc083c41a5020f6e5e916a99ee0f7c588b6affe317b96a839120bf4",
|
||||
"sha256:25543b8f2e59ddcc9929d6f6111faa5c474b21580d2996f93347bb55f2ecba84",
|
||||
"sha256:2d4609996616114c155c1e697a9faf604d81f2508cd9a4168a0bafd53c799e24",
|
||||
"sha256:2fdb2a1ed2b3e43514d9c29c9de415c953a46caabbc8a9b7de1439a0c1bd3b89",
|
||||
"sha256:3886a7983d8ae19df0c11a54114d6546fcdf76cf18cdccf25c3b14200fd5478a",
|
||||
"sha256:408d111b9341f107bdafc523e2345471547ffe8a4104e6f2ce690b7a25c4bae5",
|
||||
"sha256:60b3dd5e76c1389fc836bf83675985b92d158ff9a8d3d6d3f0a670f0c227ef13",
|
||||
"sha256:629be7ce8504530b4adbf0425a44dd53007ccb6212344804294888c9662cc38f",
|
||||
"sha256:6af3dde07b1051e954230e650a6ef74073cf993cf473c2078580f8a73c4fe46a",
|
||||
"sha256:7a1e77539d28e90517c55561f40f7872f1348d0e23f25a38d68abbfb5b0eff88",
|
||||
"sha256:87917a18b3b5951b6c9badd7b5ef09f63f61611966b58427b856bdf5c1d68e91",
|
||||
"sha256:8823d0ebd185a77edb506e286c88d06847f75620a033ad96ef9c0fd7efc1d859",
|
||||
"sha256:8bd3e12e1969beb813b861a2a65d4f2d4faaa87de0b60bf7f848da2d8ffc4eb2",
|
||||
"sha256:8f37e9acc46e75ed9786ece89afeacd86182893eacc3f0642d81531b90fbe25f",
|
||||
"sha256:9b358dd2f4142e89d760a52a7a8f4ec5dbaf955e7ada09f703f3a5d05dddd12e",
|
||||
"sha256:9cb43007c4a8aa7adaacf896f5109b578028f23d259615e3fa5866e38855b311",
|
||||
"sha256:9cf594bfbfbf84dcd462b20a4a753362be7ed376d2b5020a083dac24400b7b6c",
|
||||
"sha256:ab79940e5c5ed949e1f95e7f417dd916b0992d29f45d073dd64501a76d128e2c",
|
||||
"sha256:ba8aab6c78a82755477bb8c79f3be0824b297422d1edb21b94ae5a45407bf3ba",
|
||||
"sha256:bcc00b83bf39f6e60a13f0b24ec3951f4d2ae810b01e6e125b7ff238a85da1ac",
|
||||
"sha256:c1fcf5cbe6a2ecdc587b469156520b9128ccdb7c5908060c7d9712cd97e76db5",
|
||||
"sha256:c6e640d39b9615388b59036b29970292b15f4519043e43833e28c674f740d1f7",
|
||||
"sha256:c6ea2c385da620049b17f0135cf9307a4750e9d9c9988e15bfeeaf1f209c4ada",
|
||||
"sha256:cec4f37120f93fe2ab4ab9a7eab9a877163d74c232c93a275a624971f8557b81",
|
||||
"sha256:d2dbb42d237bcdecb7284535ec074c85bbf880124c1cbbff362ed3bd81ed7d41",
|
||||
"sha256:d5c98a41abd4f7de43b256c21bbba2a97c57e25bf6a170927a90638b18f7509c",
|
||||
"sha256:dcf5965a24179aa7dcfa00b5ff70f4f2f202e663657e0c74a642307beecda053",
|
||||
"sha256:e11e3aacf0200d6e00a9b74534e0174738768fe1c41e5aa2f4aab881d6b43afd",
|
||||
"sha256:e550816bdb2e49bba94bcd7f342004a8adbc46e9a25c8c4ed3fd58f2435c655f"
|
||||
"sha256:0286f704e55e3012fec3910400fe1a4ed11aeb66d3ec4b7f8041845af7fb7206",
|
||||
"sha256:033a4e80dc78d9c11860800bd5a66b65ff385be8f669e96b02e795364c860597",
|
||||
"sha256:0e3b5469912430f19407ebe14cfd1bece1b5a277c4d43e1b65dbff19d9475ccc",
|
||||
"sha256:131aa8c3862a555819428856f872ab9e919e351d7cd60c98012e12d2fb6afc45",
|
||||
"sha256:1783b8fa74f58a643e7780112fc4eb6110789672e852a691fad6af6b94a90c4a",
|
||||
"sha256:1e80f74854bd1c7263942e836d69f95ffc66bb45bf14bf3e1ab61113271b5884",
|
||||
"sha256:27ae784acff3d2fa04e3b4dc72f8d60a55d654f90e410adf08f46a4d2d673dd3",
|
||||
"sha256:33c6bee5a02408018dc10a5737818d2159f14cbb0613df41cc93ba6cbaeea095",
|
||||
"sha256:376a1840d1f5d25e9c3391557d6b3eeb3de17be697b0e55d8247d0262fcbaacf",
|
||||
"sha256:3922dffd8160d54dc00c7d32b30776a974cc098086493c668faffac19e752087",
|
||||
"sha256:4ba7e5afc93b413bbb5f3dd65ba583e078ff5895a5053d825ab793cf7720ae96",
|
||||
"sha256:4e9a1276f8699d06518cec8caceb2c423fc7f971765cab7550d39f281795fd81",
|
||||
"sha256:51ac9c4f8a542cd20c6776fde781c84c0acd8faba55ec14f121c6b4eb4245e89",
|
||||
"sha256:5580b86cf49936c9c74f0def44d3582a7a1bb720eba8a14805c3a61efa790c70",
|
||||
"sha256:58a879208bd84d6819a61c1b0618655574ef9df1d63a0e2f434fdcb5cfa1fb57",
|
||||
"sha256:675918f83fa35bd54f4c29d95d8652c6215d5e95a13b6f14e626cdef6d0fce79",
|
||||
"sha256:68259fd06188951d152665ffe44f9660edd715c102ae4bc4216eca4c4666dadf",
|
||||
"sha256:6cea124cbd9081a587e1954b98e9a27c7cca6ae72babc3046ab6b439a5730679",
|
||||
"sha256:6f356a445ba7afc634b1046d9f51d3ae37afbf4fe1a500285aca37677462a7b9",
|
||||
"sha256:7f7430434bd997584f2136a675559ba0d4afdf7cb71d9bbc429b0cc831e6828c",
|
||||
"sha256:809d60f15a32c21dc221ddb591aff8adfdde4e05095414eb8e015cdfef361615",
|
||||
"sha256:826c19f26b41e99691e77823ad67f04dc0b69e514212907695e330c6f106415c",
|
||||
"sha256:96c6f657b93f49243d083840d27a5a686a1fc26044a80ebf8585734d5152d4ee",
|
||||
"sha256:9a2091371298f04ef350f776365945537d0befa95bad5623d80c4207bdff9d3a",
|
||||
"sha256:9af72b764b41ba939e8e0a7ae9ec8a17d1c46a18797c6342cba6483f29e1790f",
|
||||
"sha256:a209002e3d4787f0e90e29f15cddbe83dc9054238c0da7f539c913002a348cc1",
|
||||
"sha256:a908d5af2f26673e970c7c03703437bf95d10e88dad3322e7e267467db44a04d",
|
||||
"sha256:ab841c69581085b6f9aa54044a13db6ec31183513f7cce0862d29c9b7b4e3c64",
|
||||
"sha256:b1bc78efefb8e085c072add2c02326fdecad9b8644b3be11e715ea4c6102ad87",
|
||||
"sha256:b97e74ffe121dfa9ae7ec94393fce4e95e9e0a343827663e989dc4b7c918d1a5",
|
||||
"sha256:bba8d3b61ec113bb94596599d2568217b22ddfc7baa46c00dec5106cfd4e914b",
|
||||
"sha256:bfe0e33aea60da100b214c72c1746cc0194bb8da910004518c185041cc795543",
|
||||
"sha256:c15f0718cbc3986e747d5b0734198dce0ac07d188ec5e063b1e9889ac947f86e",
|
||||
"sha256:c56d0ac769bf1f01dbb6ec6b6492849e70cd35bdeeb660e206a70ab43917ae92",
|
||||
"sha256:d396fdb7026986e6d3897bb207cc7d5bc536a82a2e50af806a24b3d254c73bc3",
|
||||
"sha256:d62ab00dea7fa0813fc813a6c848da2eeda5cb71893b892a229d23949de0cecd",
|
||||
"sha256:da75e33e185c8be17a82ec4a97f5c75ec05d57e85f8b285f86e2a22484849e4a",
|
||||
"sha256:dcbd1fbb540638c9ad9c3a071b392b654f79666a2bc12808080b0e9f674b9a80",
|
||||
"sha256:e7e90bad5466347a3648358e9f437e72d5f6d6025fe741171a88aca8b9d864df",
|
||||
"sha256:eae371a663ceeef8f930323a120a9d11e13e1c49903a66ddb4ada4830d5bcb7d",
|
||||
"sha256:f290cccc972533a288c2ebc55eb3c0fbe0c6a0d0a9775cb34ce6bfb11fe14a11",
|
||||
"sha256:facb8c588cdd6adc51ae7545f59283565dae8d946c6163e578b70ab6bf161215",
|
||||
"sha256:fb043e45f91634776acdfe4b8dfc96b636c53a458799179041ab633e15c3d833"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017",
|
||||
"sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e"
|
||||
"sha256:7782115794ec28b011702815d9f5e532244560cd2bf0789c4f09381d43befd90",
|
||||
"sha256:9e7521e9abeaede4d2d1092a106e418c65ddf6b3182b43930bcb3c8cfb974488"
|
||||
],
|
||||
"version": "==1.4.7"
|
||||
"version": "==1.4.8"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
|
||||
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==0.23"
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
|
@ -438,10 +529,10 @@
|
|||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
],
|
||||
"version": "==7.2.0"
|
||||
"version": "==8.0.2"
|
||||
},
|
||||
"nodeenv": {
|
||||
"hashes": [
|
||||
|
@ -458,39 +549,39 @@
|
|||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
|
||||
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.0"
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f",
|
||||
"sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a"
|
||||
"sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e",
|
||||
"sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.3"
|
||||
"version": "==1.20.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f",
|
||||
"sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77",
|
||||
"sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657",
|
||||
"sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896",
|
||||
"sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf",
|
||||
"sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6",
|
||||
"sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b",
|
||||
"sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300",
|
||||
"sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a",
|
||||
"sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789",
|
||||
"sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe",
|
||||
"sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc",
|
||||
"sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1",
|
||||
"sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe",
|
||||
"sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09",
|
||||
"sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de"
|
||||
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd",
|
||||
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed",
|
||||
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057",
|
||||
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce",
|
||||
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03",
|
||||
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46",
|
||||
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33",
|
||||
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c",
|
||||
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9",
|
||||
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef",
|
||||
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b",
|
||||
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d",
|
||||
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8",
|
||||
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6",
|
||||
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941",
|
||||
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13"
|
||||
],
|
||||
"version": "==3.10.0"
|
||||
"version": "==3.11.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
|
@ -515,43 +606,41 @@
|
|||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
|
||||
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
|
||||
"sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
|
||||
"sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
|
||||
],
|
||||
"version": "==2.4.2"
|
||||
"version": "==2.4.5"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
|
||||
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
|
||||
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
||||
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2.1"
|
||||
"version": "==5.3.2"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
],
|
||||
"version": "==5.1.2"
|
||||
"version": "==5.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
|
@ -562,10 +651,10 @@
|
|||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:3e3597e89c73df9313f5566e8fc582bd7037938d15b05329c232ec57a11a7ad5",
|
||||
"sha256:5d370508bf32e522d79096e8cbea3499d47e624ac7e11e9089f9397a0b3318df"
|
||||
"sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3",
|
||||
"sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"
|
||||
],
|
||||
"version": "==16.7.6"
|
||||
"version": "==16.7.9"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
|
|
0
daemon/core/gui/__init__.py
Normal file
107
daemon/core/gui/app.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui import appconfig, themes
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.menuaction import MenuAction
|
||||
from core.gui.menubar import Menubar
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.statusbar import StatusBar
|
||||
from core.gui.toolbar import Toolbar
|
||||
from core.gui.validation import InputValidation
|
||||
|
||||
WIDTH = 1000
|
||||
HEIGHT = 800
|
||||
|
||||
|
||||
class Application(tk.Frame):
|
||||
def __init__(self, master=None):
|
||||
super().__init__(master)
|
||||
# load node icons
|
||||
NodeUtils.setup()
|
||||
|
||||
# widgets
|
||||
self.menubar = None
|
||||
self.toolbar = None
|
||||
self.canvas = None
|
||||
self.statusbar = None
|
||||
self.validation = None
|
||||
|
||||
# setup
|
||||
self.guiconfig = appconfig.read()
|
||||
self.style = ttk.Style()
|
||||
self.setup_theme()
|
||||
self.core = CoreClient(self)
|
||||
self.setup_app()
|
||||
self.draw()
|
||||
self.core.set_up()
|
||||
|
||||
def setup_theme(self):
|
||||
themes.load(self.style)
|
||||
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
|
||||
self.master.bind("<<ThemeChanged>>", themes.theme_change)
|
||||
self.style.theme_use(self.guiconfig["preferences"]["theme"])
|
||||
|
||||
def setup_app(self):
|
||||
self.master.title("CORE")
|
||||
self.center()
|
||||
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
self.master.tk.call("wm", "iconphoto", self.master._w, image)
|
||||
self.pack(fill=tk.BOTH, expand=True)
|
||||
self.validation = InputValidation(self)
|
||||
|
||||
def center(self):
|
||||
screen_width = self.master.winfo_screenwidth()
|
||||
screen_height = self.master.winfo_screenheight()
|
||||
x = int((screen_width / 2) - (WIDTH / 2))
|
||||
y = int((screen_height / 2) - (HEIGHT / 2))
|
||||
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
|
||||
|
||||
def draw(self):
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.menubar = Menubar(self.master, self)
|
||||
self.toolbar = Toolbar(self, self)
|
||||
self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2)
|
||||
self.draw_canvas()
|
||||
self.draw_status()
|
||||
|
||||
def draw_canvas(self):
|
||||
width = self.guiconfig["preferences"]["width"]
|
||||
height = self.guiconfig["preferences"]["height"]
|
||||
self.canvas = CanvasGraph(self, self.core, width, height)
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
scroll_x = ttk.Scrollbar(
|
||||
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview)
|
||||
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
def draw_status(self):
|
||||
self.statusbar = StatusBar(master=self, app=self)
|
||||
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
|
||||
def on_closing(self):
|
||||
menu_action = MenuAction(self, self.master)
|
||||
menu_action.on_quit()
|
||||
|
||||
def save_config(self):
|
||||
appconfig.save(self.guiconfig)
|
||||
|
||||
def close(self):
|
||||
self.master.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
|
||||
logging.basicConfig(level=logging.DEBUG, format=log_format)
|
||||
Images.load_all()
|
||||
appconfig.check_directory()
|
||||
app = Application()
|
||||
app.mainloop()
|
111
daemon/core/gui/appconfig.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
# gui home paths
|
||||
from core.gui import themes
|
||||
|
||||
HOME_PATH = Path.home().joinpath(".coretk")
|
||||
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
|
||||
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
|
||||
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
|
||||
ICONS_PATH = HOME_PATH.joinpath("icons")
|
||||
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
||||
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
||||
|
||||
# local paths
|
||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
||||
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute()
|
||||
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute()
|
||||
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
|
||||
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
||||
|
||||
# configuration data
|
||||
TERMINALS = [
|
||||
"$TERM",
|
||||
"gnome-terminal --window --",
|
||||
"lxterminal -e",
|
||||
"konsole -e",
|
||||
"xterm -e",
|
||||
"aterm -e",
|
||||
"eterm -e",
|
||||
"rxvt -e",
|
||||
"xfce4-terminal -x",
|
||||
]
|
||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
|
||||
|
||||
class IndentDumper(yaml.Dumper):
|
||||
def increase_indent(self, flow=False, indentless=False):
|
||||
return super().increase_indent(flow, False)
|
||||
|
||||
|
||||
def copy_files(current_path, new_path):
|
||||
for current_file in current_path.glob("*"):
|
||||
new_file = new_path.joinpath(current_file.name)
|
||||
shutil.copy(current_file, new_file)
|
||||
|
||||
|
||||
def check_directory():
|
||||
if HOME_PATH.exists():
|
||||
logging.info("~/.coretk exists")
|
||||
return
|
||||
logging.info("creating ~/.coretk")
|
||||
HOME_PATH.mkdir()
|
||||
BACKGROUNDS_PATH.mkdir()
|
||||
CUSTOM_EMANE_PATH.mkdir()
|
||||
CUSTOM_SERVICE_PATH.mkdir()
|
||||
ICONS_PATH.mkdir()
|
||||
MOBILITY_PATH.mkdir()
|
||||
XMLS_PATH.mkdir()
|
||||
|
||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||
|
||||
if "TERM" in os.environ:
|
||||
terminal = TERMINALS[0]
|
||||
else:
|
||||
terminal = TERMINALS[1]
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
config = {
|
||||
"preferences": {
|
||||
"theme": themes.THEME_DARK,
|
||||
"editor": editor,
|
||||
"terminal": terminal,
|
||||
"gui3d": "/usr/local/bin/std3d.sh",
|
||||
"width": 1000,
|
||||
"height": 750,
|
||||
},
|
||||
"location": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"lat": 47.5791667,
|
||||
"lon": -122.132322,
|
||||
"alt": 2.0,
|
||||
"scale": 150.0,
|
||||
},
|
||||
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
|
||||
"nodes": [],
|
||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||
}
|
||||
save(config)
|
||||
|
||||
|
||||
def read():
|
||||
with CONFIG_PATH.open("r") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
|
||||
def save(config):
|
||||
with CONFIG_PATH.open("w") as f:
|
||||
yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)
|
909
daemon/core/gui/coreclient.py
Normal file
|
@ -0,0 +1,909 @@
|
|||
"""
|
||||
Incorporate grpc into python tkinter GUI
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import client, core_pb2
|
||||
from core.gui import appconfig
|
||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.shape import AnnotationData, Shape
|
||||
from core.gui.graph.shapeutils import ShapeType
|
||||
from core.gui.interface import InterfaceManager
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
|
||||
GUI_SOURCE = "gui"
|
||||
OBSERVERS = {
|
||||
"processes": "ps",
|
||||
"ifconfig": "ifconfig",
|
||||
"IPV4 Routes": "ip -4 ro",
|
||||
"IPV6 Routes": "ip -6 ro",
|
||||
"Listening sockets": "netstat -tuwnl",
|
||||
"IPv4 MFC entries": "ip -4 mroute show",
|
||||
"IPv6 MFC entries": "ip -6 mroute show",
|
||||
"firewall rules": "iptables -L",
|
||||
"IPSec policies": "setkey -DP",
|
||||
}
|
||||
|
||||
|
||||
class CoreServer:
|
||||
def __init__(self, name, address, port):
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.port = port
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, name, cmd):
|
||||
self.name = name
|
||||
self.cmd = cmd
|
||||
|
||||
|
||||
class CoreClient:
|
||||
def __init__(self, app):
|
||||
"""
|
||||
Create a CoreGrpc instance
|
||||
"""
|
||||
self.client = client.CoreGrpcClient()
|
||||
self.session_id = None
|
||||
self.node_ids = []
|
||||
self.app = app
|
||||
self.master = app.master
|
||||
self.services = {}
|
||||
self.default_services = {}
|
||||
self.emane_models = []
|
||||
self.observer = None
|
||||
|
||||
# loaded configuration data
|
||||
self.servers = {}
|
||||
self.custom_nodes = {}
|
||||
self.custom_observers = {}
|
||||
self.read_config()
|
||||
|
||||
# helpers
|
||||
self.interface_to_edge = {}
|
||||
self.interfaces_manager = InterfaceManager(self.app)
|
||||
|
||||
# session data
|
||||
self.state = None
|
||||
self.canvas_nodes = {}
|
||||
self.location = None
|
||||
self.links = {}
|
||||
self.hooks = {}
|
||||
self.wlan_configs = {}
|
||||
self.mobility_configs = {}
|
||||
self.emane_model_configs = {}
|
||||
self.emane_config = None
|
||||
self.service_configs = {}
|
||||
self.file_configs = {}
|
||||
self.mobility_players = {}
|
||||
self.handling_throughputs = None
|
||||
self.handling_events = None
|
||||
|
||||
def reset(self):
|
||||
# helpers
|
||||
self.interfaces_manager.reset()
|
||||
self.interface_to_edge.clear()
|
||||
# session data
|
||||
self.canvas_nodes.clear()
|
||||
self.links.clear()
|
||||
self.hooks.clear()
|
||||
self.wlan_configs.clear()
|
||||
self.mobility_configs.clear()
|
||||
self.emane_model_configs.clear()
|
||||
self.emane_config = None
|
||||
self.service_configs.clear()
|
||||
self.file_configs.clear()
|
||||
self.mobility_players.clear()
|
||||
# clear streams
|
||||
if self.handling_throughputs:
|
||||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
if self.handling_events:
|
||||
self.handling_events.cancel()
|
||||
self.handling_events = None
|
||||
|
||||
def set_observer(self, value):
|
||||
self.observer = value
|
||||
|
||||
def read_config(self):
|
||||
# read distributed server
|
||||
for config in self.app.guiconfig.get("servers", []):
|
||||
server = CoreServer(config["name"], config["address"], config["port"])
|
||||
self.servers[server.name] = server
|
||||
|
||||
# read custom nodes
|
||||
for config in self.app.guiconfig.get("nodes", []):
|
||||
name = config["name"]
|
||||
image_file = config["image"]
|
||||
services = set(config["services"])
|
||||
node_draw = NodeDraw.from_custom(name, image_file, services)
|
||||
self.custom_nodes[name] = node_draw
|
||||
|
||||
# read observers
|
||||
for config in self.app.guiconfig.get("observers", []):
|
||||
observer = Observer(config["name"], config["cmd"])
|
||||
self.custom_observers[observer.name] = observer
|
||||
|
||||
def handle_events(self, event):
|
||||
if event.session_id != self.session_id:
|
||||
logging.warn(
|
||||
"ignoring event session(%s) current(%s)",
|
||||
event.session_id,
|
||||
self.session_id,
|
||||
)
|
||||
return
|
||||
|
||||
if event.HasField("link_event"):
|
||||
logging.info("link event: %s", event)
|
||||
self.handle_link_event(event.link_event)
|
||||
elif event.HasField("session_event"):
|
||||
logging.info("session event: %s", event)
|
||||
session_event = event.session_event
|
||||
if session_event.event <= core_pb2.SessionState.SHUTDOWN:
|
||||
self.state = event.session_event.event
|
||||
elif session_event.event in {7, 8, 9}:
|
||||
node_id = session_event.node_id
|
||||
dialog = self.mobility_players.get(node_id)
|
||||
if dialog:
|
||||
if session_event.event == 7:
|
||||
dialog.set_play()
|
||||
elif session_event.event == 8:
|
||||
dialog.set_stop()
|
||||
else:
|
||||
dialog.set_pause()
|
||||
else:
|
||||
logging.warning("unknown session event: %s", session_event)
|
||||
elif event.HasField("node_event"):
|
||||
self.handle_node_event(event.node_event)
|
||||
elif event.HasField("config_event"):
|
||||
logging.info("config event: %s", event)
|
||||
elif event.HasField("exception_event"):
|
||||
self.handle_exception_event(event.exception_event)
|
||||
else:
|
||||
logging.info("unhandled event: %s", event)
|
||||
|
||||
def handle_link_event(self, event):
|
||||
node_one_id = event.link.node_one_id
|
||||
node_two_id = event.link.node_two_id
|
||||
canvas_node_one = self.canvas_nodes[node_one_id]
|
||||
canvas_node_two = self.canvas_nodes[node_two_id]
|
||||
|
||||
if event.message_type == core_pb2.MessageType.ADD:
|
||||
self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two)
|
||||
elif event.message_type == core_pb2.MessageType.DELETE:
|
||||
self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two)
|
||||
else:
|
||||
logging.warning("unknown link event: %s", event.message_type)
|
||||
|
||||
def handle_node_event(self, event):
|
||||
if event.source == GUI_SOURCE:
|
||||
return
|
||||
node_id = event.node.id
|
||||
x = event.node.position.x
|
||||
y = event.node.position.y
|
||||
canvas_node = self.canvas_nodes[node_id]
|
||||
canvas_node.move(x, y)
|
||||
|
||||
def enable_throughputs(self):
|
||||
self.handling_throughputs = self.client.throughputs(
|
||||
self.session_id, self.handle_throughputs
|
||||
)
|
||||
|
||||
def cancel_throughputs(self):
|
||||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
|
||||
def handle_throughputs(self, event):
|
||||
if event.session_id != self.session_id:
|
||||
logging.warn(
|
||||
"ignoring throughput event session(%s) current(%s)",
|
||||
event.session_id,
|
||||
self.session_id,
|
||||
)
|
||||
return
|
||||
logging.info("handling throughputs event: %s", event)
|
||||
self.app.canvas.throughput_draw.process_grpc_throughput_event(
|
||||
event.interface_throughputs
|
||||
)
|
||||
|
||||
def handle_exception_event(self, event):
|
||||
logging.info("exception event: %s", event)
|
||||
self.app.statusbar.core_alarms.append(event)
|
||||
|
||||
def join_session(self, session_id, query_location=True):
|
||||
# update session and title
|
||||
self.session_id = session_id
|
||||
self.master.title(f"CORE Session({self.session_id})")
|
||||
|
||||
# clear session data
|
||||
self.reset()
|
||||
|
||||
# get session data
|
||||
try:
|
||||
response = self.client.get_session(self.session_id)
|
||||
session = response.session
|
||||
self.state = session.state
|
||||
self.handling_events = self.client.events(
|
||||
self.session_id, self.handle_events
|
||||
)
|
||||
|
||||
# get location
|
||||
if query_location:
|
||||
response = self.client.get_session_location(self.session_id)
|
||||
self.location = response.location
|
||||
|
||||
# get emane models
|
||||
response = self.client.get_emane_models(self.session_id)
|
||||
self.emane_models = response.models
|
||||
|
||||
# get hooks
|
||||
response = self.client.get_hooks(self.session_id)
|
||||
for hook in response.hooks:
|
||||
self.hooks[hook.file] = hook
|
||||
|
||||
# get mobility configs
|
||||
response = self.client.get_mobility_configs(self.session_id)
|
||||
for node_id in response.configs:
|
||||
node_config = response.configs[node_id].config
|
||||
self.mobility_configs[node_id] = node_config
|
||||
|
||||
# get emane config
|
||||
response = self.client.get_emane_config(self.session_id)
|
||||
self.emane_config = response.config
|
||||
|
||||
# get emane model config
|
||||
response = self.client.get_emane_model_configs(self.session_id)
|
||||
for config in response.configs:
|
||||
interface = None
|
||||
if config.interface != -1:
|
||||
interface = config.interface
|
||||
self.set_emane_model_config(
|
||||
config.node_id, config.model, config.config, interface
|
||||
)
|
||||
|
||||
# get wlan configurations
|
||||
response = self.client.get_wlan_configs(self.session_id)
|
||||
for _id in response.configs:
|
||||
mapped_config = response.configs[_id]
|
||||
self.wlan_configs[_id] = mapped_config.config
|
||||
|
||||
# get service configurations
|
||||
response = self.client.get_node_service_configs(self.session_id)
|
||||
for config in response.configs:
|
||||
service_configs = self.service_configs.setdefault(config.node_id, {})
|
||||
service_configs[config.service] = config.data
|
||||
logging.info("service file configs: %s", config.files)
|
||||
for file_name in config.files:
|
||||
file_configs = self.file_configs.setdefault(config.node_id, {})
|
||||
files = file_configs.setdefault(config.service, {})
|
||||
data = config.files[file_name]
|
||||
files[file_name] = data
|
||||
|
||||
# draw session
|
||||
self.app.canvas.reset_and_redraw(session)
|
||||
|
||||
# get metadata
|
||||
response = self.client.get_session_metadata(self.session_id)
|
||||
self.parse_metadata(response.config)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
# update ui to represent current state
|
||||
if self.is_runtime():
|
||||
self.app.toolbar.runtime_frame.tkraise()
|
||||
self.app.toolbar.click_runtime_selection()
|
||||
else:
|
||||
self.app.toolbar.design_frame.tkraise()
|
||||
self.app.toolbar.click_selection()
|
||||
self.app.statusbar.progress_bar.stop()
|
||||
|
||||
def is_runtime(self):
|
||||
return self.state == core_pb2.SessionState.RUNTIME
|
||||
|
||||
def parse_metadata(self, config):
|
||||
# canvas setting
|
||||
canvas_config = config.get("canvas")
|
||||
logging.info("canvas metadata: %s", canvas_config)
|
||||
if canvas_config:
|
||||
canvas_config = json.loads(canvas_config)
|
||||
|
||||
gridlines = canvas_config.get("gridlines", True)
|
||||
self.app.canvas.show_grid.set(gridlines)
|
||||
|
||||
fit_image = canvas_config.get("fit_image", False)
|
||||
self.app.canvas.adjust_to_dim.set(fit_image)
|
||||
|
||||
wallpaper_style = canvas_config.get("wallpaper-style", 1)
|
||||
self.app.canvas.scale_option.set(wallpaper_style)
|
||||
|
||||
width = self.app.guiconfig["preferences"]["width"]
|
||||
height = self.app.guiconfig["preferences"]["height"]
|
||||
dimensions = canvas_config.get("dimensions", [width, height])
|
||||
self.app.canvas.redraw_canvas(dimensions)
|
||||
|
||||
wallpaper = canvas_config.get("wallpaper")
|
||||
if wallpaper:
|
||||
wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper))
|
||||
self.app.canvas.set_wallpaper(wallpaper)
|
||||
else:
|
||||
self.app.canvas.redraw_canvas()
|
||||
self.app.canvas.set_wallpaper(None)
|
||||
|
||||
# load saved shapes
|
||||
shapes_config = config.get("shapes")
|
||||
if shapes_config:
|
||||
shapes_config = json.loads(shapes_config)
|
||||
for shape_config in shapes_config:
|
||||
logging.info("loading shape: %s", shape_config)
|
||||
shape_type = shape_config["type"]
|
||||
try:
|
||||
shape_type = ShapeType(shape_type)
|
||||
coords = shape_config["iconcoords"]
|
||||
data = AnnotationData(
|
||||
shape_config["label"],
|
||||
shape_config["fontfamily"],
|
||||
shape_config["fontsize"],
|
||||
shape_config["labelcolor"],
|
||||
shape_config["color"],
|
||||
shape_config["border"],
|
||||
shape_config["width"],
|
||||
shape_config["bold"],
|
||||
shape_config["italic"],
|
||||
shape_config["underline"],
|
||||
)
|
||||
shape = Shape(
|
||||
self.app, self.app.canvas, shape_type, *coords, data=data
|
||||
)
|
||||
self.app.canvas.shapes[shape.id] = shape
|
||||
except ValueError:
|
||||
logging.exception("unknown shape: %s", shape_type)
|
||||
|
||||
for tag in tags.ABOVE_WALLPAPER_TAGS:
|
||||
self.app.canvas.tag_raise(tag)
|
||||
|
||||
def create_new_session(self):
|
||||
"""
|
||||
Create a new session
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
response = self.client.create_session()
|
||||
logging.info("created session: %s", response)
|
||||
location_config = self.app.guiconfig["location"]
|
||||
self.location = core_pb2.SessionLocation(
|
||||
x=location_config["x"],
|
||||
y=location_config["y"],
|
||||
z=location_config["z"],
|
||||
lat=location_config["lat"],
|
||||
lon=location_config["lon"],
|
||||
alt=location_config["alt"],
|
||||
scale=location_config["scale"],
|
||||
)
|
||||
self.join_session(response.session_id, query_location=False)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def delete_session(self, session_id=None):
|
||||
if session_id is None:
|
||||
session_id = self.session_id
|
||||
try:
|
||||
response = self.client.delete_session(session_id)
|
||||
logging.info("deleted session result: %s", response)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def set_up(self):
|
||||
"""
|
||||
Query sessions, if there exist any, prompt whether to join one
|
||||
|
||||
:return: existing sessions
|
||||
"""
|
||||
try:
|
||||
self.client.connect()
|
||||
|
||||
# get service information
|
||||
response = self.client.get_services()
|
||||
for service in response.services:
|
||||
group_services = self.services.setdefault(service.group, set())
|
||||
group_services.add(service.name)
|
||||
|
||||
# if there are no sessions, create a new session, else join a session
|
||||
response = self.client.get_sessions()
|
||||
logging.info("current sessions: %s", response)
|
||||
sessions = response.sessions
|
||||
if len(sessions) == 0:
|
||||
self.create_new_session()
|
||||
else:
|
||||
dialog = SessionsDialog(self.app, self.app)
|
||||
dialog.show()
|
||||
|
||||
response = self.client.get_service_defaults(self.session_id)
|
||||
self.default_services = {
|
||||
x.node_type: set(x.services) for x in response.defaults
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.app.close()
|
||||
|
||||
def edit_node(self, core_node):
|
||||
try:
|
||||
self.client.edit_node(
|
||||
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def start_session(self):
|
||||
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
||||
links = [x.link for x in self.links.values()]
|
||||
wlan_configs = self.get_wlan_configs_proto()
|
||||
mobility_configs = self.get_mobility_configs_proto()
|
||||
emane_model_configs = self.get_emane_model_configs_proto()
|
||||
hooks = list(self.hooks.values())
|
||||
service_configs = self.get_service_configs_proto()
|
||||
file_configs = self.get_service_file_configs_proto()
|
||||
asymmetric_links = [
|
||||
x.asymmetric_link for x in self.links.values() if x.asymmetric_link
|
||||
]
|
||||
if self.emane_config:
|
||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
else:
|
||||
emane_config = None
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
response = self.client.start_session(
|
||||
self.session_id,
|
||||
nodes,
|
||||
links,
|
||||
self.location,
|
||||
hooks,
|
||||
emane_config,
|
||||
emane_model_configs,
|
||||
wlan_configs,
|
||||
mobility_configs,
|
||||
service_configs,
|
||||
file_configs,
|
||||
asymmetric_links,
|
||||
)
|
||||
self.set_metadata()
|
||||
process_time = time.perf_counter() - start
|
||||
logging.debug(
|
||||
"start session(%s), result: %s", self.session_id, response.result
|
||||
)
|
||||
self.app.statusbar.start_session_callback(process_time)
|
||||
|
||||
# display mobility players
|
||||
for node_id, config in self.mobility_configs.items():
|
||||
canvas_node = self.canvas_nodes[node_id]
|
||||
mobility_player = MobilityPlayer(
|
||||
self.app, self.app, canvas_node, config
|
||||
)
|
||||
mobility_player.show()
|
||||
self.mobility_players[node_id] = mobility_player
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def stop_session(self, session_id=None):
|
||||
if not session_id:
|
||||
session_id = self.session_id
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
response = self.client.stop_session(session_id)
|
||||
logging.debug(
|
||||
"stopped session(%s), result: %s", session_id, response.result
|
||||
)
|
||||
process_time = time.perf_counter() - start
|
||||
self.app.statusbar.stop_session_callback(process_time)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def set_metadata(self):
|
||||
# create canvas data
|
||||
wallpaper = None
|
||||
if self.app.canvas.wallpaper_file:
|
||||
wallpaper = Path(self.app.canvas.wallpaper_file).name
|
||||
canvas_config = {
|
||||
"wallpaper": wallpaper,
|
||||
"wallpaper-style": self.app.canvas.scale_option.get(),
|
||||
"gridlines": self.app.canvas.show_grid.get(),
|
||||
"fit_image": self.app.canvas.adjust_to_dim.get(),
|
||||
"dimensions": self.app.canvas.current_dimensions,
|
||||
}
|
||||
canvas_config = json.dumps(canvas_config)
|
||||
|
||||
# create shapes data
|
||||
shapes = []
|
||||
for shape in self.app.canvas.shapes.values():
|
||||
shapes.append(shape.metadata())
|
||||
shapes = json.dumps(shapes)
|
||||
|
||||
metadata = {"canvas": canvas_config, "shapes": shapes}
|
||||
response = self.client.set_session_metadata(self.session_id, metadata)
|
||||
logging.info("set session metadata: %s", response)
|
||||
|
||||
def launch_terminal(self, node_id):
|
||||
try:
|
||||
response = self.client.get_node_terminal(self.session_id, node_id)
|
||||
logging.info("get terminal %s", response.terminal)
|
||||
os.system(f"xterm -e {response.terminal} &")
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def save_xml(self, file_path):
|
||||
"""
|
||||
Save core session as to an xml file
|
||||
|
||||
:param str file_path: file path that user pick
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
if self.state != core_pb2.SessionState.RUNTIME:
|
||||
logging.debug(
|
||||
"session state not runtime, send session data to the daemon..."
|
||||
)
|
||||
self.send_data()
|
||||
response = self.client.save_xml(self.session_id, file_path)
|
||||
logging.info("saved xml(%s): %s", file_path, response)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def open_xml(self, file_path):
|
||||
"""
|
||||
Open core xml
|
||||
|
||||
:param str file_path: file to open
|
||||
:return: session id
|
||||
"""
|
||||
try:
|
||||
response = self.client.open_xml(file_path)
|
||||
logging.debug("open xml: %s", response)
|
||||
self.join_session(response.session_id)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def get_node_service(self, node_id, service_name):
|
||||
response = self.client.get_node_service(self.session_id, node_id, service_name)
|
||||
logging.debug("get node service %s", response)
|
||||
return response.service
|
||||
|
||||
def set_node_service(self, node_id, service_name, startups, validations, shutdowns):
|
||||
response = self.client.set_node_service(
|
||||
self.session_id, node_id, service_name, startups, validations, shutdowns
|
||||
)
|
||||
logging.debug("set node service %s", response)
|
||||
response = self.client.get_node_service(self.session_id, node_id, service_name)
|
||||
logging.debug("get node service : %s", response)
|
||||
return response.service
|
||||
|
||||
def get_node_service_file(self, node_id, service_name, file_name):
|
||||
response = self.client.get_node_service_file(
|
||||
self.session_id, node_id, service_name, file_name
|
||||
)
|
||||
logging.debug("get service file %s", response)
|
||||
return response.data
|
||||
|
||||
def set_node_service_file(self, node_id, service_name, file_name, data):
|
||||
response = self.client.set_node_service_file(
|
||||
self.session_id, node_id, service_name, file_name, data
|
||||
)
|
||||
logging.debug("set node service file %s", response)
|
||||
|
||||
def create_nodes_and_links(self):
|
||||
"""
|
||||
create nodes and links that have not been created yet
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
||||
link_protos = [x.link for x in self.links.values()]
|
||||
if self.state != core_pb2.SessionState.DEFINITION:
|
||||
self.client.set_session_state(
|
||||
self.session_id, core_pb2.SessionState.DEFINITION
|
||||
)
|
||||
|
||||
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION)
|
||||
for node_proto in node_protos:
|
||||
response = self.client.add_node(self.session_id, node_proto)
|
||||
logging.debug("create node: %s", response)
|
||||
for link_proto in link_protos:
|
||||
response = self.client.add_link(
|
||||
self.session_id,
|
||||
link_proto.node_one_id,
|
||||
link_proto.node_two_id,
|
||||
link_proto.interface_one,
|
||||
link_proto.interface_two,
|
||||
link_proto.options,
|
||||
)
|
||||
logging.debug("create link: %s", response)
|
||||
|
||||
def send_data(self):
|
||||
"""
|
||||
send to daemon all session info, but don't start the session
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.create_nodes_and_links()
|
||||
for config_proto in self.get_wlan_configs_proto():
|
||||
self.client.set_wlan_config(
|
||||
self.session_id, config_proto.node_id, config_proto.config
|
||||
)
|
||||
for config_proto in self.get_mobility_configs_proto():
|
||||
self.client.set_mobility_config(
|
||||
self.session_id, config_proto.node_id, config_proto.config
|
||||
)
|
||||
for config_proto in self.get_service_configs_proto():
|
||||
self.client.set_node_service(
|
||||
self.session_id,
|
||||
config_proto.node_id,
|
||||
config_proto.service,
|
||||
config_proto.startup,
|
||||
config_proto.validate,
|
||||
config_proto.shutdown,
|
||||
)
|
||||
for config_proto in self.get_service_file_configs_proto():
|
||||
self.client.set_node_service_file(
|
||||
self.session_id,
|
||||
config_proto.node_id,
|
||||
config_proto.service,
|
||||
config_proto.file,
|
||||
config_proto.data,
|
||||
)
|
||||
for hook in self.hooks.values():
|
||||
self.client.add_hook(self.session_id, hook.state, hook.file, hook.data)
|
||||
for config_proto in self.get_emane_model_configs_proto():
|
||||
self.client.set_emane_model_config(
|
||||
self.session_id,
|
||||
config_proto.node_id,
|
||||
config_proto.model,
|
||||
config_proto.config,
|
||||
config_proto.interface_id,
|
||||
)
|
||||
if self.emane_config:
|
||||
config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
self.client.set_emane_config(self.session_id, config)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Clean ups when done using grpc
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("close grpc")
|
||||
self.client.close()
|
||||
|
||||
def next_node_id(self):
|
||||
"""
|
||||
Get the next usable node id.
|
||||
|
||||
:return: the next id to be used
|
||||
:rtype: int
|
||||
"""
|
||||
i = 1
|
||||
while True:
|
||||
if i not in self.canvas_nodes:
|
||||
break
|
||||
i += 1
|
||||
return i
|
||||
|
||||
def create_node(self, x, y, node_type, model):
|
||||
"""
|
||||
Add node, with information filled in, to grpc manager
|
||||
|
||||
:param int x: x coord
|
||||
:param int y: y coord
|
||||
:param core_pb2.NodeType node_type: node type
|
||||
:param str model: node model
|
||||
:return: nothing
|
||||
"""
|
||||
node_id = self.next_node_id()
|
||||
position = core_pb2.Position(x=x, y=y)
|
||||
image = None
|
||||
if NodeUtils.is_image_node(node_type):
|
||||
image = "ubuntu:latest"
|
||||
emane = None
|
||||
if node_type == core_pb2.NodeType.EMANE:
|
||||
emane = self.emane_models[0]
|
||||
node = core_pb2.Node(
|
||||
id=node_id,
|
||||
type=node_type,
|
||||
name=f"n{node_id}",
|
||||
model=model,
|
||||
position=position,
|
||||
image=image,
|
||||
emane=emane,
|
||||
)
|
||||
logging.debug(
|
||||
"adding node to core session: %s, coords: (%s, %s), name: %s",
|
||||
self.session_id,
|
||||
x,
|
||||
y,
|
||||
node.name,
|
||||
)
|
||||
return node
|
||||
|
||||
def delete_graph_nodes(self, canvas_nodes):
|
||||
"""
|
||||
remove the nodes selected by the user and anything related to that node
|
||||
such as link, configurations, interfaces
|
||||
|
||||
:param list canvas_nodes: list of nodes to delete
|
||||
:return: nothing
|
||||
"""
|
||||
edges = set()
|
||||
for canvas_node in canvas_nodes:
|
||||
node_id = canvas_node.core_node.id
|
||||
if node_id not in self.canvas_nodes:
|
||||
logging.error("unknown node: %s", node_id)
|
||||
continue
|
||||
del self.canvas_nodes[node_id]
|
||||
if node_id in self.mobility_configs:
|
||||
del self.mobility_configs[node_id]
|
||||
if node_id in self.wlan_configs:
|
||||
del self.wlan_configs[node_id]
|
||||
for key in list(self.emane_model_configs):
|
||||
node_id, _, _ = key
|
||||
if node_id == node_id:
|
||||
del self.emane_model_configs[key]
|
||||
|
||||
for edge in canvas_node.edges:
|
||||
if edge in edges:
|
||||
continue
|
||||
edges.add(edge)
|
||||
if edge.token not in self.links:
|
||||
logging.error("unknown edge: %s", edge.token)
|
||||
del self.links[edge.token]
|
||||
|
||||
def create_interface(self, canvas_node):
|
||||
node = canvas_node.core_node
|
||||
ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id)
|
||||
interface_id = len(canvas_node.interfaces)
|
||||
name = f"eth{interface_id}"
|
||||
interface = core_pb2.Interface(
|
||||
id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix
|
||||
)
|
||||
canvas_node.interfaces.append(interface)
|
||||
logging.debug(
|
||||
"create node(%s) interface IPv4: %s, name: %s",
|
||||
node.name,
|
||||
interface.ip4,
|
||||
interface.name,
|
||||
)
|
||||
return interface
|
||||
|
||||
def create_link(self, edge, canvas_src_node, canvas_dst_node):
|
||||
"""
|
||||
Create core link for a pair of canvas nodes, with token referencing
|
||||
the canvas edge.
|
||||
|
||||
:param edge: edge for link
|
||||
:param canvas_src_node: canvas node one
|
||||
:param canvas_dst_node: canvas node two
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
src_node = canvas_src_node.core_node
|
||||
dst_node = canvas_dst_node.core_node
|
||||
|
||||
# determine subnet
|
||||
self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node)
|
||||
|
||||
src_interface = None
|
||||
if NodeUtils.is_container_node(src_node.type):
|
||||
src_interface = self.create_interface(canvas_src_node)
|
||||
edge.src_interface = src_interface
|
||||
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token
|
||||
|
||||
dst_interface = None
|
||||
if NodeUtils.is_container_node(dst_node.type):
|
||||
dst_interface = self.create_interface(canvas_dst_node)
|
||||
edge.dst_interface = dst_interface
|
||||
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token
|
||||
|
||||
link = core_pb2.Link(
|
||||
type=core_pb2.LinkType.WIRED,
|
||||
node_one_id=src_node.id,
|
||||
node_two_id=dst_node.id,
|
||||
interface_one=src_interface,
|
||||
interface_two=dst_interface,
|
||||
)
|
||||
edge.set_link(link)
|
||||
self.links[edge.token] = edge
|
||||
|
||||
def get_wlan_configs_proto(self):
|
||||
configs = []
|
||||
for node_id, config in self.wlan_configs.items():
|
||||
config = {x: config[x].value for x in config}
|
||||
wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config)
|
||||
configs.append(wlan_config)
|
||||
return configs
|
||||
|
||||
def get_mobility_configs_proto(self):
|
||||
configs = []
|
||||
for node_id, config in self.mobility_configs.items():
|
||||
config = {x: config[x].value for x in config}
|
||||
mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config)
|
||||
configs.append(mobility_config)
|
||||
return configs
|
||||
|
||||
def get_emane_model_configs_proto(self):
|
||||
configs = []
|
||||
for key, config in self.emane_model_configs.items():
|
||||
node_id, model, interface = key
|
||||
config = {x: config[x].value for x in config}
|
||||
if interface is None:
|
||||
interface = -1
|
||||
config_proto = core_pb2.EmaneModelConfig(
|
||||
node_id=node_id, interface_id=interface, model=model, config=config
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_service_configs_proto(self):
|
||||
configs = []
|
||||
for node_id, services in self.service_configs.items():
|
||||
for name, config in services.items():
|
||||
config_proto = core_pb2.ServiceConfig(
|
||||
node_id=node_id,
|
||||
service=name,
|
||||
startup=config.startup,
|
||||
validate=config.validate,
|
||||
shutdown=config.shutdown,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_service_file_configs_proto(self):
|
||||
configs = []
|
||||
for (node_id, file_configs) in self.file_configs.items():
|
||||
for service, file_config in file_configs.items():
|
||||
for file, data in file_config.items():
|
||||
config_proto = core_pb2.ServiceFileConfig(
|
||||
node_id=node_id, service=service, file=file, data=data
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def run(self, node_id):
|
||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||
|
||||
def get_wlan_config(self, node_id):
|
||||
config = self.wlan_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
return config
|
||||
|
||||
def get_mobility_config(self, node_id):
|
||||
config = self.mobility_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
return config
|
||||
|
||||
def get_emane_model_config(self, node_id, model, interface=None):
|
||||
logging.info("getting emane model config: %s %s %s", node_id, model, interface)
|
||||
config = self.emane_model_configs.get((node_id, model, interface))
|
||||
if not config:
|
||||
if interface is None:
|
||||
interface = -1
|
||||
response = self.client.get_emane_model_config(
|
||||
self.session_id, node_id, model, interface
|
||||
)
|
||||
config = response.config
|
||||
return config
|
||||
|
||||
def set_emane_model_config(self, node_id, model, config, interface=None):
|
||||
logging.info("setting emane model config: %s %s %s", node_id, model, interface)
|
||||
self.emane_model_configs[(node_id, model, interface)] = config
|
BIN
daemon/core/gui/data/backgrounds/sample1-bg.gif
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
daemon/core/gui/data/backgrounds/sample4-bg.jpg
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
daemon/core/gui/data/icons/OVS.gif
Executable file
After Width: | Height: | Size: 744 B |
BIN
daemon/core/gui/data/icons/alert.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/antenna.gif
Normal file
After Width: | Height: | Size: 230 B |
BIN
daemon/core/gui/data/icons/core-icon.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
daemon/core/gui/data/icons/docker.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/document-new.gif
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/document-properties.gif
Normal file
After Width: | Height: | Size: 635 B |
BIN
daemon/core/gui/data/icons/document-save.gif
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/edit-delete.gif
Normal file
After Width: | Height: | Size: 1,006 B |
BIN
daemon/core/gui/data/icons/edit-node.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
daemon/core/gui/data/icons/emane.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
daemon/core/gui/data/icons/fileopen.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/host.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
daemon/core/gui/data/icons/hub.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
daemon/core/gui/data/icons/lanswitch.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/link.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
daemon/core/gui/data/icons/lxc.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
daemon/core/gui/data/icons/marker.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/icons/markerclear.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
daemon/core/gui/data/icons/mdr.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
daemon/core/gui/data/icons/observe.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/oval.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
daemon/core/gui/data/icons/pause.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
daemon/core/gui/data/icons/pc.png
Normal file
After Width: | Height: | Size: 828 B |
BIN
daemon/core/gui/data/icons/plot.gif
Normal file
After Width: | Height: | Size: 265 B |
BIN
daemon/core/gui/data/icons/prouter.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
daemon/core/gui/data/icons/rectangle.png
Normal file
After Width: | Height: | Size: 259 B |
BIN
daemon/core/gui/data/icons/rj45.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/router.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/run.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
daemon/core/gui/data/icons/select.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/start.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
daemon/core/gui/data/icons/stop.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
daemon/core/gui/data/icons/text.png
Normal file
After Width: | Height: | Size: 314 B |
BIN
daemon/core/gui/data/icons/tunnel.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
daemon/core/gui/data/icons/twonode.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
daemon/core/gui/data/icons/wlan.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
28
daemon/core/gui/data/mobility/sample1.scen
Normal file
|
@ -0,0 +1,28 @@
|
|||
#
|
||||
# nodes: 4, max time: 27.000000, max x: 600.00, max y: 600.00
|
||||
# nominal range: 300.00 link bw: 54000000.00
|
||||
# pause: 30.00, min speed 1.50 max speed: 4.50
|
||||
|
||||
$node_(6) set X_ 780.0
|
||||
$node_(6) set Y_ 228.0
|
||||
$node_(6) set Z_ 0.00
|
||||
$node_(7) set X_ 816.0
|
||||
$node_(7) set Y_ 348.0
|
||||
$node_(7) set Z_ 0.00
|
||||
$node_(8) set X_ 672.0
|
||||
$node_(8) set Y_ 420.0
|
||||
$node_(8) set Z_ 0.00
|
||||
$node_(9) set X_ 672.0
|
||||
$node_(9) set Y_ 96.0
|
||||
$node_(9) set Z_ 0.00
|
||||
$ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0"
|
||||
$ns_ at 2.00 "$node_(7) setdest 400.0 288.0 15.0"
|
||||
$ns_ at 1.00 "$node_(8) setdest 590.0 520.0 17.0"
|
||||
$ns_ at 3.00 "$node_(9) setdest 720.0 300.0 20.0"
|
||||
$ns_ at 8.00 "$node_(7) setdest 600.0 350.0 10.0"
|
||||
$ns_ at 9.00 "$node_(8) setdest 730.0 300.0 15.0"
|
||||
$ns_ at 10.00 "$node_(6) setdest 600.0 108.0 10.0"
|
||||
$ns_ at 16.00 "$node_(9) setdest 672.0 96.0 20.0"
|
||||
$ns_ at 17.00 "$node_(7) setdest 816.0 348.0 20.0"
|
||||
$ns_ at 18.00 "$node_(6) setdest 780.0 228.0 25.0"
|
||||
$ns_ at 22.00 "$node_(8) setdest 672.0 420.0 20.0"
|
BIN
daemon/core/gui/data/oldicons/docker.gif
Normal file
After Width: | Height: | Size: 719 B |
BIN
daemon/core/gui/data/oldicons/emane.gif
Normal file
After Width: | Height: | Size: 337 B |
BIN
daemon/core/gui/data/oldicons/host.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/hub.gif
Normal file
After Width: | Height: | Size: 719 B |
BIN
daemon/core/gui/data/oldicons/lanswitch.gif
Normal file
After Width: | Height: | Size: 744 B |
BIN
daemon/core/gui/data/oldicons/link.gif
Normal file
After Width: | Height: | Size: 86 B |
BIN
daemon/core/gui/data/oldicons/lxc.gif
Normal file
After Width: | Height: | Size: 724 B |
BIN
daemon/core/gui/data/oldicons/marker.gif
Normal file
After Width: | Height: | Size: 375 B |
BIN
daemon/core/gui/data/oldicons/mdr.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/oval.gif
Normal file
After Width: | Height: | Size: 174 B |
BIN
daemon/core/gui/data/oldicons/pc.gif
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
daemon/core/gui/data/oldicons/rectangle.gif
Normal file
After Width: | Height: | Size: 160 B |
BIN
daemon/core/gui/data/oldicons/rj45.gif
Normal file
After Width: | Height: | Size: 755 B |
BIN
daemon/core/gui/data/oldicons/router.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/oldicons/router_green.gif
Normal file
After Width: | Height: | Size: 753 B |
BIN
daemon/core/gui/data/oldicons/run.gif
Normal file
After Width: | Height: | Size: 324 B |
BIN
daemon/core/gui/data/oldicons/select.gif
Normal file
After Width: | Height: | Size: 925 B |
BIN
daemon/core/gui/data/oldicons/start.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/oldicons/stop.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/text.gif
Normal file
After Width: | Height: | Size: 127 B |
BIN
daemon/core/gui/data/oldicons/tunnel.gif
Normal file
After Width: | Height: | Size: 799 B |
BIN
daemon/core/gui/data/oldicons/twonode.gif
Normal file
After Width: | Height: | Size: 220 B |
BIN
daemon/core/gui/data/oldicons/wlan.gif
Normal file
After Width: | Height: | Size: 173 B |
1869
daemon/core/gui/data/xmls/sample1.xml
Normal file
0
daemon/core/gui/dialogs/__init__.py
Normal file
44
daemon/core/gui/dialogs/about.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import tkinter as tk
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
LICENSE = """\
|
||||
Copyright (c) 2005-2020, the Boeing Company.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.\
|
||||
"""
|
||||
|
||||
|
||||
class AboutDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "About CORE", modal=True)
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
codetext = CodeText(self.top)
|
||||
codetext.text.insert("1.0", LICENSE)
|
||||
codetext.text.config(state=tk.DISABLED)
|
||||
codetext.grid(sticky="nsew")
|
171
daemon/core/gui/dialogs/alerts.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
"""
|
||||
check engine light
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from grpc import RpcError
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
|
||||
class AlertsDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Alerts", modal=True)
|
||||
self.app = app
|
||||
self.tree = None
|
||||
self.codetext = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame,
|
||||
columns=("time", "level", "session_id", "node", "source"),
|
||||
show="headings",
|
||||
)
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
self.tree.column("time", stretch=tk.YES)
|
||||
self.tree.heading("time", text="Time")
|
||||
self.tree.column("level", stretch=tk.YES)
|
||||
self.tree.heading("level", text="Level")
|
||||
self.tree.column("session_id", stretch=tk.YES)
|
||||
self.tree.heading("session_id", text="Session ID")
|
||||
self.tree.column("node", stretch=tk.YES)
|
||||
self.tree.heading("node", text="Node")
|
||||
self.tree.column("source", stretch=tk.YES)
|
||||
self.tree.heading("source", text="Source")
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
for alarm in self.app.statusbar.core_alarms:
|
||||
level = self.get_level(alarm.level)
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
text=str(alarm.date),
|
||||
values=(
|
||||
alarm.date,
|
||||
level + " (%s)" % alarm.level,
|
||||
alarm.session_id,
|
||||
alarm.node_id,
|
||||
alarm.source,
|
||||
),
|
||||
tags=(level,),
|
||||
)
|
||||
|
||||
self.tree.tag_configure("ERROR", background="#ff6666")
|
||||
self.tree.tag_configure("FATAL", background="#d9d9d9")
|
||||
self.tree.tag_configure("WARNING", background="#ffff99")
|
||||
self.tree.tag_configure("NOTICE", background="#85e085")
|
||||
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
self.codetext = CodeText(self.top)
|
||||
self.codetext.text.config(state=tk.DISABLED)
|
||||
self.codetext.grid(sticky="nsew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Node Log")
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Close", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def reset_alerts(self):
|
||||
self.codetext.text.delete("1.0", tk.END)
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
self.app.statusbar.core_alarms.clear()
|
||||
|
||||
def daemon_log(self):
|
||||
dialog = DaemonLog(self, self.app)
|
||||
dialog.show()
|
||||
|
||||
def get_level(self, level):
|
||||
if level == core_pb2.ExceptionLevel.ERROR:
|
||||
return "ERROR"
|
||||
if level == core_pb2.ExceptionLevel.FATAL:
|
||||
return "FATAL"
|
||||
if level == core_pb2.ExceptionLevel.WARNING:
|
||||
return "WARNING"
|
||||
if level == core_pb2.ExceptionLevel.NOTICE:
|
||||
return "NOTICE"
|
||||
|
||||
def click_select(self, event):
|
||||
current = self.tree.selection()
|
||||
values = self.tree.item(current)["values"]
|
||||
time = values[0]
|
||||
level = values[1]
|
||||
session_id = values[2]
|
||||
node_id = values[3]
|
||||
source = values[4]
|
||||
text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % (
|
||||
time,
|
||||
level,
|
||||
node_id,
|
||||
self.app.core.canvas_nodes[node_id].core_node.name,
|
||||
session_id,
|
||||
source,
|
||||
)
|
||||
try:
|
||||
sid = self.app.core.session_id
|
||||
self.app.core.client.get_node(sid, node_id)
|
||||
text = text + "node created"
|
||||
except RpcError:
|
||||
text = text + "node not created"
|
||||
self.codetext.text.delete("1.0", "end")
|
||||
self.codetext.text.insert("1.0", text)
|
||||
|
||||
|
||||
class DaemonLog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "core-daemon log", modal=True)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.path = tk.StringVar(value="/var/log/core-daemon.log")
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=9)
|
||||
label = ttk.Label(frame, text="File", anchor="w")
|
||||
label.grid(row=0, column=0, sticky="ew")
|
||||
entry = ttk.Entry(frame, textvariable=self.path, state="disabled")
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
try:
|
||||
file = open("/var/log/core-daemon.log", "r")
|
||||
log = file.readlines()
|
||||
except FileNotFoundError:
|
||||
log = "Log file not found"
|
||||
codetext = CodeText(self.top)
|
||||
codetext.text.insert("1.0", log)
|
||||
codetext.text.see("end")
|
||||
codetext.text.config(state=tk.DISABLED)
|
||||
codetext.grid(row=1, column=0, sticky="nsew")
|
254
daemon/core/gui/dialogs/canvassizeandscale.py
Normal file
|
@ -0,0 +1,254 @@
|
|||
"""
|
||||
size and scale
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import font, ttk
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
PIXEL_SCALE = 100
|
||||
|
||||
|
||||
class SizeAndScaleDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
"""
|
||||
create an instance for size and scale object
|
||||
|
||||
:param app: main application
|
||||
"""
|
||||
super().__init__(master, app, "Canvas Size and Scale", modal=True)
|
||||
self.canvas = self.app.canvas
|
||||
self.validation = app.validation
|
||||
self.section_font = font.Font(weight="bold")
|
||||
width, height = self.canvas.current_dimensions
|
||||
self.pixel_width = tk.IntVar(value=width)
|
||||
self.pixel_height = tk.IntVar(value=height)
|
||||
location = self.app.core.location
|
||||
self.x = tk.DoubleVar(value=location.x)
|
||||
self.y = tk.DoubleVar(value=location.y)
|
||||
self.lat = tk.DoubleVar(value=location.lat)
|
||||
self.lon = tk.DoubleVar(value=location.lon)
|
||||
self.alt = tk.DoubleVar(value=location.alt)
|
||||
self.scale = tk.DoubleVar(value=location.scale)
|
||||
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale)
|
||||
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale)
|
||||
self.save_default = tk.BooleanVar(value=False)
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.draw_size()
|
||||
self.draw_scale()
|
||||
self.draw_reference_point()
|
||||
self.draw_save_as_default()
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_size(self):
|
||||
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# draw size row 1
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.pixel_width,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.pixel_height,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="Pixels")
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
|
||||
# draw size row 2
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.meters_width,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.meters_height,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
|
||||
def draw_scale(self):
|
||||
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.scale,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=2, sticky="w")
|
||||
|
||||
def draw_reference_point(self):
|
||||
label_frame = ttk.Labelframe(
|
||||
self.top, text="Reference Point", padding=FRAME_PAD
|
||||
)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
label = ttk.Label(
|
||||
label_frame, text="Default is (0, 0), the upper left corner of the canvas"
|
||||
)
|
||||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="X")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.x,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Y")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.y,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(label_frame, text="Translates To")
|
||||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.columnconfigure(5, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Lat")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.lat,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Lon")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.lon,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Alt")
|
||||
label.grid(row=0, column=4, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.alt,
|
||||
validate="key",
|
||||
validatecommand=(self.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
|
||||
entry.grid(row=0, column=5, sticky="ew")
|
||||
|
||||
def draw_save_as_default(self):
|
||||
button = ttk.Checkbutton(
|
||||
self.top, text="Save as default?", variable=self.save_default
|
||||
)
|
||||
button.grid(sticky="w", pady=PADY)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
width, height = self.pixel_width.get(), self.pixel_height.get()
|
||||
self.canvas.redraw_canvas((width, height))
|
||||
if self.canvas.wallpaper:
|
||||
self.canvas.redraw_wallpaper()
|
||||
location = self.app.core.location
|
||||
location.x = self.x.get()
|
||||
location.y = self.y.get()
|
||||
location.lat = self.lat.get()
|
||||
location.lon = self.lon.get()
|
||||
location.alt = self.alt.get()
|
||||
location.scale = self.scale.get()
|
||||
if self.save_default.get():
|
||||
location_config = self.app.guiconfig["location"]
|
||||
location_config["x"] = location.x
|
||||
location_config["y"] = location.y
|
||||
location_config["z"] = location.z
|
||||
location_config["lat"] = location.lat
|
||||
location_config["lon"] = location.lon
|
||||
location_config["alt"] = location.alt
|
||||
location_config["scale"] = location.scale
|
||||
preferences = self.app.guiconfig["preferences"]
|
||||
preferences["width"] = width
|
||||
preferences["height"] = height
|
||||
self.app.save_config()
|
||||
self.destroy()
|
179
daemon/core/gui/dialogs/canvaswallpaper.py
Normal file
|
@ -0,0 +1,179 @@
|
|||
"""
|
||||
set wallpaper
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
|
||||
class CanvasBackgroundDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
"""
|
||||
create an instance of CanvasWallpaper object
|
||||
|
||||
:param coretk.app.Application app: root application
|
||||
"""
|
||||
super().__init__(master, app, "Canvas Background", modal=True)
|
||||
self.canvas = self.app.canvas
|
||||
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get())
|
||||
self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get())
|
||||
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get())
|
||||
self.filename = tk.StringVar(value=self.canvas.wallpaper_file)
|
||||
self.image_label = None
|
||||
self.options = []
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.draw_image()
|
||||
self.draw_image_label()
|
||||
self.draw_image_selection()
|
||||
self.draw_options()
|
||||
self.draw_additional_options()
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_image(self):
|
||||
self.image_label = ttk.Label(
|
||||
self.top, text="(image preview)", width=32, anchor=tk.CENTER
|
||||
)
|
||||
self.image_label.grid(pady=PADY)
|
||||
|
||||
def draw_image_label(self):
|
||||
label = ttk.Label(self.top, text="Image filename: ")
|
||||
label.grid(sticky="ew")
|
||||
if self.filename.get():
|
||||
self.draw_preview()
|
||||
|
||||
def draw_image_selection(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=2)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
entry = ttk.Entry(frame, textvariable=self.filename)
|
||||
entry.focus()
|
||||
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="...", command=self.click_open_image)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Clear", command=self.click_clear)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_options(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="upper-left", value=1, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="centered", value=2, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="scaled", value=3, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="titled", value=4, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
def draw_additional_options(self):
|
||||
checkbutton = ttk.Checkbutton(
|
||||
self.top, text="Show grid", variable=self.show_grid
|
||||
)
|
||||
checkbutton.grid(sticky="ew", padx=PADX)
|
||||
|
||||
checkbutton = ttk.Checkbutton(
|
||||
self.top,
|
||||
text="Adjust canvas size to image dimensions",
|
||||
variable=self.adjust_to_dim,
|
||||
command=self.click_adjust_canvas,
|
||||
)
|
||||
checkbutton.grid(sticky="ew", padx=PADX)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_open_image(self):
|
||||
filename = image_chooser(self, BACKGROUNDS_PATH)
|
||||
if filename:
|
||||
self.filename.set(filename)
|
||||
self.draw_preview()
|
||||
|
||||
def draw_preview(self):
|
||||
image = Images.create(self.filename.get(), 250, 135)
|
||||
self.image_label.config(image=image)
|
||||
self.image_label.image = image
|
||||
|
||||
def click_clear(self):
|
||||
"""
|
||||
delete like shown in image link entry if there is any
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# delete entry
|
||||
self.filename.set("")
|
||||
# delete display image
|
||||
self.image_label.config(image="", width=32)
|
||||
self.image_label.image = None
|
||||
|
||||
def click_adjust_canvas(self):
|
||||
# deselect all radio buttons and grey them out
|
||||
if self.adjust_to_dim.get():
|
||||
self.scale_option.set(0)
|
||||
for option in self.options:
|
||||
option.config(state=tk.DISABLED)
|
||||
# turn back the radio button to active state so that user can choose again
|
||||
else:
|
||||
self.scale_option.set(1)
|
||||
for option in self.options:
|
||||
option.config(state=tk.NORMAL)
|
||||
|
||||
def click_apply(self):
|
||||
self.canvas.scale_option.set(self.scale_option.get())
|
||||
self.canvas.show_grid.set(self.show_grid.get())
|
||||
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
||||
self.canvas.update_grid()
|
||||
|
||||
filename = self.filename.get()
|
||||
if not filename:
|
||||
filename = None
|
||||
|
||||
try:
|
||||
self.canvas.set_wallpaper(filename)
|
||||
except FileNotFoundError:
|
||||
logging.error("invalid background: %s", filename)
|
||||
|
||||
self.destroy()
|
251
daemon/core/gui/dialogs/colorpicker.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
"""
|
||||
custom color picker
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
||||
|
||||
class ColorPicker(Dialog):
|
||||
def __init__(self, master, app, initcolor="#000000"):
|
||||
super().__init__(master, app, "color picker", modal=True)
|
||||
self.red_entry = None
|
||||
self.blue_entry = None
|
||||
self.green_entry = None
|
||||
self.hex_entry = None
|
||||
self.red_label = None
|
||||
self.green_label = None
|
||||
self.blue_label = None
|
||||
self.display = None
|
||||
self.color = initcolor
|
||||
red, green, blue = self.get_rgb(initcolor)
|
||||
self.red = tk.IntVar(value=red)
|
||||
self.blue = tk.IntVar(value=blue)
|
||||
self.green = tk.IntVar(value=green)
|
||||
self.hex = tk.StringVar(value=initcolor)
|
||||
self.red_scale = tk.IntVar(value=red)
|
||||
self.green_scale = tk.IntVar(value=green)
|
||||
self.blue_scale = tk.IntVar(value=blue)
|
||||
self.draw()
|
||||
self.set_bindings()
|
||||
|
||||
def askcolor(self):
|
||||
self.show()
|
||||
return self.color
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
# rgb frames
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=6)
|
||||
frame.columnconfigure(3, weight=2)
|
||||
label = ttk.Label(frame, text="R: ")
|
||||
label.grid(row=0, column=0)
|
||||
self.red_entry = ttk.Entry(
|
||||
frame,
|
||||
width=4,
|
||||
textvariable=self.red,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.rgb, "%P"),
|
||||
)
|
||||
self.red_entry.grid(row=0, column=1, sticky="nsew")
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
to=255,
|
||||
value=0,
|
||||
# length=200,
|
||||
orient=tk.HORIZONTAL,
|
||||
variable=self.red_scale,
|
||||
command=lambda x: self.scale_callback(self.red_scale, self.red),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="nsew")
|
||||
self.red_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
|
||||
)
|
||||
self.red_label.grid(row=0, column=3, sticky="nsew")
|
||||
frame.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=6)
|
||||
frame.columnconfigure(3, weight=2)
|
||||
label = ttk.Label(frame, text="G: ")
|
||||
label.grid(row=0, column=0)
|
||||
self.green_entry = ttk.Entry(
|
||||
frame,
|
||||
width=4,
|
||||
textvariable=self.green,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.rgb, "%P"),
|
||||
)
|
||||
self.green_entry.grid(row=0, column=1, sticky="nsew")
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
to=255,
|
||||
value=0,
|
||||
# length=200,
|
||||
orient=tk.HORIZONTAL,
|
||||
variable=self.green_scale,
|
||||
command=lambda x: self.scale_callback(self.green_scale, self.green),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="nsew")
|
||||
self.green_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
|
||||
)
|
||||
self.green_label.grid(row=0, column=3, sticky="nsew")
|
||||
frame.grid(row=1, column=0, sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=6)
|
||||
frame.columnconfigure(3, weight=2)
|
||||
label = ttk.Label(frame, text="B: ")
|
||||
label.grid(row=0, column=0)
|
||||
self.blue_entry = ttk.Entry(
|
||||
frame,
|
||||
width=4,
|
||||
textvariable=self.blue,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.rgb, "%P"),
|
||||
)
|
||||
self.blue_entry.grid(row=0, column=1, sticky="nsew")
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
to=255,
|
||||
value=0,
|
||||
# length=200,
|
||||
orient=tk.HORIZONTAL,
|
||||
variable=self.blue_scale,
|
||||
command=lambda x: self.scale_callback(self.blue_scale, self.blue),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="nsew")
|
||||
self.blue_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
|
||||
)
|
||||
self.blue_label.grid(row=0, column=3, sticky="nsew")
|
||||
frame.grid(row=2, column=0, sticky="nsew")
|
||||
|
||||
# hex code and color display
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
label = ttk.Label(frame, text="Selection: ")
|
||||
label.grid(row=0, column=0, sticky="nsew")
|
||||
self.hex_entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.hex,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.hex, "%P"),
|
||||
)
|
||||
self.hex_entry.grid(row=1, column=0, sticky="nsew")
|
||||
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
|
||||
self.display.grid(row=2, column=0)
|
||||
frame.grid(row=3, column=0, sticky="nsew")
|
||||
|
||||
# button frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="OK", command=self.button_ok)
|
||||
button.grid(row=0, column=0, sticky="nsew")
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="nsew")
|
||||
frame.grid(row=4, column=0, sticky="nsew")
|
||||
|
||||
def set_bindings(self):
|
||||
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||
self.hex_entry.bind("<FocusIn>", lambda x: self.current_focus("hex"))
|
||||
self.red.trace_add("write", self.update_color)
|
||||
self.green.trace_add("write", self.update_color)
|
||||
self.blue.trace_add("write", self.update_color)
|
||||
self.hex.trace_add("write", self.update_color)
|
||||
|
||||
def button_ok(self):
|
||||
logging.debug("not implemented")
|
||||
self.color = self.hex.get()
|
||||
self.destroy()
|
||||
|
||||
def get_hex(self):
|
||||
"""
|
||||
convert current RGB values into hex color
|
||||
|
||||
:rtype: str
|
||||
:return: hex color
|
||||
"""
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
|
||||
def current_focus(self, focus):
|
||||
self.focus = focus
|
||||
|
||||
def update_color(self, arg1=None, arg2=None, arg3=None):
|
||||
if self.focus == "rgb":
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
self.set_scale(red, green, blue)
|
||||
if red and blue and green:
|
||||
hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
self.hex.set(hex_code)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
elif self.focus == "hex":
|
||||
hex_code = self.hex.get()
|
||||
if len(hex_code) == 4 or len(hex_code) == 7:
|
||||
red, green, blue = self.get_rgb(hex_code)
|
||||
else:
|
||||
return
|
||||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
|
||||
def scale_callback(self, var, color_var):
|
||||
color_var.set(var.get())
|
||||
self.focus = "rgb"
|
||||
self.update_color()
|
||||
|
||||
def set_scale(self, red, green, blue):
|
||||
self.red_scale.set(red)
|
||||
self.green_scale.set(green)
|
||||
self.blue_scale.set(blue)
|
||||
|
||||
def set_entry(self, red, green, blue):
|
||||
self.red.set(red)
|
||||
self.green.set(green)
|
||||
self.blue.set(blue)
|
||||
|
||||
def set_label(self, red, green, blue):
|
||||
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
||||
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
||||
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
||||
|
||||
def get_rgb(self, hex_code):
|
||||
"""
|
||||
convert a valid hex code to RGB values
|
||||
|
||||
:param string hex_code: color in hex
|
||||
:rtype: tuple(int, int, int)
|
||||
:return: the RGB values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
green = hex_code[2]
|
||||
blue = hex_code[3]
|
||||
else:
|
||||
red = hex_code[1:3]
|
||||
green = hex_code[3:5]
|
||||
blue = hex_code[5:]
|
||||
return int(red, 16), int(green, 16), int(blue, 16)
|
261
daemon/core/gui/dialogs/customnodes.py
Normal file
|
@ -0,0 +1,261 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
||||
|
||||
class ServicesSelectDialog(Dialog):
|
||||
def __init__(self, master, app, current_services):
|
||||
super().__init__(master, app, "Node Services", modal=True)
|
||||
self.groups = None
|
||||
self.services = None
|
||||
self.current = None
|
||||
self.current_services = set(current_services)
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.LabelFrame(self.top)
|
||||
frame.grid(stick="nsew", pady=PADY)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
for group in sorted(self.app.core.services):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.destroy)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event):
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.services[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
def service_clicked(self, name, var):
|
||||
if var.get() and name not in self.current_services:
|
||||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.current.listbox.delete(0, tk.END)
|
||||
for name in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, name)
|
||||
|
||||
def click_cancel(self):
|
||||
self.current_services = None
|
||||
self.destroy()
|
||||
|
||||
|
||||
class CustomNodesDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Custom Nodes", modal=True)
|
||||
self.edit_button = None
|
||||
self.delete_button = None
|
||||
self.nodes_list = None
|
||||
self.name = tk.StringVar()
|
||||
self.image_button = None
|
||||
self.image = None
|
||||
self.image_file = None
|
||||
self.services = set()
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.draw_node_config()
|
||||
self.draw_node_buttons()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_node_config(self):
|
||||
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.nodes_list = ListboxScroll(frame)
|
||||
self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX)
|
||||
self.nodes_list.listbox.bind("<<ListboxSelect>>", self.handle_node_select)
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
|
||||
frame = ttk.Frame(frame)
|
||||
frame.grid(row=0, column=2, sticky="nsew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(sticky="ew")
|
||||
self.image_button = ttk.Button(
|
||||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
self.image_button.grid(sticky="ew")
|
||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||
button.grid(sticky="ew")
|
||||
|
||||
def draw_node_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
self.edit_button = ttk.Button(
|
||||
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
|
||||
)
|
||||
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def reset_values(self):
|
||||
self.name.set("")
|
||||
self.image = None
|
||||
self.image_file = None
|
||||
self.services = set()
|
||||
self.image_button.config(image="")
|
||||
|
||||
def click_icon(self):
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
self.image = image
|
||||
self.image_file = file_path
|
||||
self.image_button.config(image=self.image)
|
||||
|
||||
def click_services(self):
|
||||
dialog = ServicesSelectDialog(self, self.app, self.services)
|
||||
dialog.show()
|
||||
if dialog.current_services is not None:
|
||||
self.services.clear()
|
||||
self.services.update(dialog.current_services)
|
||||
|
||||
def click_save(self):
|
||||
self.app.guiconfig["nodes"].clear()
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
node_draw = self.app.core.custom_nodes[name]
|
||||
self.app.guiconfig["nodes"].append(
|
||||
{
|
||||
"name": name,
|
||||
"image": node_draw.image_file,
|
||||
"services": list(node_draw.services),
|
||||
}
|
||||
)
|
||||
logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"])
|
||||
self.app.save_config()
|
||||
self.destroy()
|
||||
|
||||
def click_create(self):
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_nodes:
|
||||
image_file = Path(self.image_file).stem
|
||||
node_draw = NodeDraw.from_custom(name, image_file, set(self.services))
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
self.reset_values()
|
||||
|
||||
def click_edit(self):
|
||||
name = self.name.get()
|
||||
if self.selected:
|
||||
previous_name = self.selected
|
||||
self.selected = name
|
||||
node_draw = self.app.core.custom_nodes.pop(previous_name)
|
||||
node_draw.model = name
|
||||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image = self.image
|
||||
node_draw.services = self.services
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
self.nodes_list.listbox.insert(self.selected_index, name)
|
||||
self.nodes_list.listbox.selection_set(self.selected_index)
|
||||
|
||||
def click_delete(self):
|
||||
if self.selected and self.selected in self.app.core.custom_nodes:
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
del self.app.core.custom_nodes[self.selected]
|
||||
self.reset_values()
|
||||
self.nodes_list.listbox.selection_clear(0, tk.END)
|
||||
self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_node_select(self, event):
|
||||
selection = self.nodes_list.listbox.curselection()
|
||||
if selection:
|
||||
self.selected_index = selection[0]
|
||||
self.selected = self.nodes_list.listbox.get(self.selected_index)
|
||||
node_draw = self.app.core.custom_nodes[self.selected]
|
||||
self.name.set(node_draw.model)
|
||||
self.services = node_draw.services
|
||||
self.image = node_draw.image
|
||||
self.image_file = node_draw.image_file
|
||||
self.image_button.config(image=self.image)
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
37
daemon/core/gui/dialogs/dialog.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import DIALOG_PAD
|
||||
|
||||
|
||||
class Dialog(tk.Toplevel):
|
||||
def __init__(self, master, app, title, modal=False):
|
||||
super().__init__(master)
|
||||
self.withdraw()
|
||||
self.app = app
|
||||
self.modal = modal
|
||||
self.title(title)
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
self.tk.call("wm", "iconphoto", self._w, image)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.top = ttk.Frame(self, padding=DIALOG_PAD)
|
||||
self.top.grid(sticky="nsew")
|
||||
|
||||
def show(self):
|
||||
self.transient(self.master)
|
||||
self.focus_force()
|
||||
self.update()
|
||||
self.deiconify()
|
||||
if self.modal:
|
||||
self.wait_visibility()
|
||||
self.grab_set()
|
||||
self.wait_window()
|
||||
|
||||
def draw_spacer(self, row=None):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=row, sticky="nsew")
|
||||
frame.rowconfigure(0, weight=1)
|
||||
self.top.rowconfigure(frame.grid_info()["row"], weight=1)
|
235
daemon/core/gui/dialogs/emaneconfig.py
Normal file
|
@ -0,0 +1,235 @@
|
|||
"""
|
||||
emane configuration
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
||||
class GlobalEmaneDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "EMANE Configuration", modal=True)
|
||||
self.config_frame = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
self.config_frame.parse_config()
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneModelDialog(Dialog):
|
||||
def __init__(self, master, app, node, model, interface=None):
|
||||
super().__init__(master, app, f"{node.name} {model} Configuration", modal=True)
|
||||
self.node = node
|
||||
self.model = f"emane_{model}"
|
||||
self.interface = interface
|
||||
self.config_frame = None
|
||||
try:
|
||||
self.config = self.app.core.get_emane_model_config(
|
||||
self.node.id, self.model, self.interface
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
self.config_frame.parse_config()
|
||||
self.app.core.set_emane_model_config(
|
||||
self.node.id, self.model, self.config, self.interface
|
||||
)
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneConfigDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node):
|
||||
super().__init__(
|
||||
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
|
||||
)
|
||||
self.app = app
|
||||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.radiovar = tk.IntVar()
|
||||
self.radiovar.set(1)
|
||||
self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models]
|
||||
self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1])
|
||||
self.emane_model_button = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.draw_emane_configuration()
|
||||
self.draw_emane_models()
|
||||
self.draw_emane_buttons()
|
||||
self.draw_spacer()
|
||||
self.draw_apply_and_cancel()
|
||||
|
||||
def draw_emane_configuration(self):
|
||||
"""
|
||||
draw the main frame for emane configuration
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
label = ttk.Label(
|
||||
self.top,
|
||||
text="The EMANE emulation system provides more complex wireless radio emulation "
|
||||
"\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details",
|
||||
justify=tk.CENTER,
|
||||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
self.top,
|
||||
image=image,
|
||||
text="EMANE Wiki",
|
||||
compound=tk.RIGHT,
|
||||
command=lambda: webbrowser.open_new(
|
||||
"https://github.com/adjacentlink/emane/wiki"
|
||||
),
|
||||
)
|
||||
button.image = image
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
|
||||
def draw_emane_models(self):
|
||||
"""
|
||||
create a combobox that has all the known emane models
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Model")
|
||||
label.grid(row=0, column=0, sticky="w")
|
||||
|
||||
# create combo box and its binding
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.emane_model,
|
||||
values=self.emane_models,
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky="ew")
|
||||
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
|
||||
|
||||
def draw_emane_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
self.emane_model_button = ttk.Button(
|
||||
frame,
|
||||
text=f"{self.emane_model.get()} options",
|
||||
image=image,
|
||||
compound=tk.RIGHT,
|
||||
command=self.click_model_config,
|
||||
)
|
||||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
frame,
|
||||
text="EMANE options",
|
||||
image=image,
|
||||
compound=tk.RIGHT,
|
||||
command=self.click_emane_config,
|
||||
)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def draw_apply_and_cancel(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_emane_config(self):
|
||||
dialog = GlobalEmaneDialog(self, self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_model_config(self):
|
||||
"""
|
||||
draw emane model configuration
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
logging.info("configuring emane model: %s", model_name)
|
||||
dialog = EmaneModelDialog(
|
||||
self, self.app, self.canvas_node.core_node, model_name
|
||||
)
|
||||
dialog.show()
|
||||
|
||||
def emane_model_change(self, event):
|
||||
"""
|
||||
update emane model options button
|
||||
|
||||
:param event:
|
||||
:return: nothing
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
self.emane_model_button.config(text=f"{model_name} options")
|
||||
|
||||
def click_apply(self):
|
||||
self.node.emane = f"emane_{self.emane_model.get()}"
|
||||
self.destroy()
|
152
daemon/core/gui/dialogs/hooks.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
|
||||
class HookDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Hook", modal=True)
|
||||
self.name = tk.StringVar()
|
||||
self.codetext = None
|
||||
self.hook = core_pb2.Hook()
|
||||
self.state = tk.StringVar()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
|
||||
# name and states
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=2)
|
||||
frame.columnconfigure(1, weight=7)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE")
|
||||
initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME)
|
||||
self.state.set(initial_state)
|
||||
self.name.set(f"{initial_state.lower()}_hook.sh")
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.state, values=values, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=2, sticky="ew")
|
||||
combobox.bind("<<ComboboxSelected>>", self.state_change)
|
||||
|
||||
# data
|
||||
self.codetext = CodeText(self.top)
|
||||
self.codetext.text.insert(
|
||||
1.0,
|
||||
(
|
||||
"#!/bin/sh\n"
|
||||
"# session hook script; write commands here to execute on the host at the\n"
|
||||
"# specified state\n"
|
||||
),
|
||||
)
|
||||
self.codetext.grid(sticky="nsew", pady=PADY)
|
||||
|
||||
# button row
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=lambda: self.save())
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def state_change(self, event):
|
||||
state_name = self.state.get()
|
||||
self.name.set(f"{state_name.lower()}_hook.sh")
|
||||
|
||||
def set(self, hook):
|
||||
self.hook = hook
|
||||
self.name.set(hook.file)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
self.codetext.text.insert(tk.END, hook.data)
|
||||
state_name = core_pb2.SessionState.Enum.Name(hook.state)
|
||||
self.state.set(state_name)
|
||||
|
||||
def save(self):
|
||||
data = self.codetext.text.get("1.0", tk.END).strip()
|
||||
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
|
||||
self.hook.file = self.name.get()
|
||||
self.hook.data = data
|
||||
self.hook.state = state_value
|
||||
self.destroy()
|
||||
|
||||
|
||||
class HooksDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Hooks", modal=True)
|
||||
self.listbox = None
|
||||
self.edit_button = None
|
||||
self.delete_button = None
|
||||
self.selected = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
self.listbox = listbox_scroll.listbox
|
||||
self.listbox.bind("<<ListboxSelect>>", self.select)
|
||||
for hook_file in self.app.core.hooks:
|
||||
self.listbox.insert(tk.END, hook_file)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
self.edit_button = ttk.Button(
|
||||
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
|
||||
)
|
||||
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_create(self):
|
||||
dialog = HookDialog(self, self.app)
|
||||
dialog.show()
|
||||
hook = dialog.hook
|
||||
if hook:
|
||||
self.app.core.hooks[hook.file] = hook
|
||||
self.listbox.insert(tk.END, hook.file)
|
||||
|
||||
def click_edit(self):
|
||||
hook = self.app.core.hooks[self.selected]
|
||||
dialog = HookDialog(self, self.app)
|
||||
dialog.set(hook)
|
||||
dialog.show()
|
||||
|
||||
def click_delete(self):
|
||||
del self.app.core.hooks[self.selected]
|
||||
self.listbox.delete(tk.ANCHOR)
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def select(self, event):
|
||||
if self.listbox.curselection():
|
||||
index = self.listbox.curselection()[0]
|
||||
self.selected = self.listbox.get(index)
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.selected = None
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
362
daemon/core/gui/dialogs/linkconfig.py
Normal file
|
@ -0,0 +1,362 @@
|
|||
"""
|
||||
link configuration
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.colorpicker import ColorPicker
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
||||
|
||||
def get_int(var):
|
||||
value = var.get()
|
||||
if value != "":
|
||||
return int(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_float(var):
|
||||
value = var.get()
|
||||
if value != "":
|
||||
return float(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class LinkConfiguration(Dialog):
|
||||
def __init__(self, master, app, edge):
|
||||
super().__init__(master, app, "Link Configuration", modal=True)
|
||||
self.app = app
|
||||
self.edge = edge
|
||||
self.is_symmetric = edge.link.options.unidirectional is False
|
||||
if self.is_symmetric:
|
||||
self.symmetry_var = tk.StringVar(value=">>")
|
||||
else:
|
||||
self.symmetry_var = tk.StringVar(value="<<")
|
||||
|
||||
self.bandwidth = tk.StringVar()
|
||||
self.delay = tk.StringVar()
|
||||
self.jitter = tk.StringVar()
|
||||
self.loss = tk.StringVar()
|
||||
self.duplicate = tk.StringVar()
|
||||
|
||||
self.down_bandwidth = tk.StringVar()
|
||||
self.down_delay = tk.StringVar()
|
||||
self.down_jitter = tk.StringVar()
|
||||
self.down_loss = tk.StringVar()
|
||||
self.down_duplicate = tk.StringVar()
|
||||
|
||||
self.color = tk.StringVar(value="#000000")
|
||||
self.color_button = None
|
||||
self.width = tk.DoubleVar()
|
||||
|
||||
self.load_link_config()
|
||||
self.symmetric_frame = None
|
||||
self.asymmetric_frame = None
|
||||
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
source_name = self.app.canvas.nodes[self.edge.src].core_node.name
|
||||
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
|
||||
label = ttk.Label(
|
||||
self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(row=0, column=0, sticky="ew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
|
||||
button = ttk.Button(frame, text="Unlimited")
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
if self.is_symmetric:
|
||||
button = ttk.Button(
|
||||
frame, textvariable=self.symmetry_var, command=self.change_symmetry
|
||||
)
|
||||
else:
|
||||
button = ttk.Button(
|
||||
frame, textvariable=self.symmetry_var, command=self.change_symmetry
|
||||
)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
if self.is_symmetric:
|
||||
self.symmetric_frame = self.get_frame()
|
||||
self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
else:
|
||||
self.asymmetric_frame = self.get_frame()
|
||||
self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
|
||||
self.draw_spacer(row=3)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=4, column=0, sticky="ew")
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def get_frame(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
if self.is_symmetric:
|
||||
label_name = "Symmetric Link Effects"
|
||||
else:
|
||||
label_name = "Asymmetric Effects: Downstream / Upstream "
|
||||
row = 0
|
||||
label = ttk.Label(frame, text=label_name, anchor=tk.CENTER)
|
||||
label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Bandwidth (bps)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.bandwidth,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.down_bandwidth,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Delay (us)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.delay,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.down_delay,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Jitter (us)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.jitter,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.down_jitter,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Loss (%)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.loss,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.down_loss,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Duplicate (%)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.duplicate,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.down_duplicate,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Color")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
self.color_button = tk.Button(
|
||||
frame,
|
||||
textvariable=self.color,
|
||||
background=self.color.get(),
|
||||
bd=0,
|
||||
relief=tk.FLAT,
|
||||
highlightthickness=0,
|
||||
command=self.click_color,
|
||||
)
|
||||
self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.width,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_float, "%P"),
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
return frame
|
||||
|
||||
def click_color(self):
|
||||
dialog = ColorPicker(self, self.app, self.color.get())
|
||||
color = dialog.askcolor()
|
||||
self.color.set(color)
|
||||
self.color_button.config(background=color)
|
||||
|
||||
def click_apply(self):
|
||||
logging.debug("click apply")
|
||||
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
|
||||
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
|
||||
link = self.edge.link
|
||||
bandwidth = get_int(self.bandwidth)
|
||||
jitter = get_int(self.jitter)
|
||||
delay = get_int(self.delay)
|
||||
duplicate = get_int(self.duplicate)
|
||||
loss = get_float(self.loss)
|
||||
options = core_pb2.LinkOptions(
|
||||
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss
|
||||
)
|
||||
link.options.CopyFrom(options)
|
||||
|
||||
interface_one = None
|
||||
if link.HasField("interface_one"):
|
||||
interface_one = link.interface_one.id
|
||||
interface_two = None
|
||||
if link.HasField("interface_two"):
|
||||
interface_two = link.interface_two.id
|
||||
|
||||
if not self.is_symmetric:
|
||||
link.options.unidirectional = True
|
||||
asym_interface_one = None
|
||||
if interface_one:
|
||||
asym_interface_one = core_pb2.Interface(id=interface_one)
|
||||
asym_interface_two = None
|
||||
if interface_two:
|
||||
asym_interface_two = core_pb2.Interface(id=interface_two)
|
||||
down_bandwidth = get_int(self.down_bandwidth)
|
||||
down_jitter = get_int(self.down_jitter)
|
||||
down_delay = get_int(self.down_delay)
|
||||
down_duplicate = get_int(self.down_duplicate)
|
||||
down_loss = get_float(self.down_loss)
|
||||
options = core_pb2.LinkOptions(
|
||||
bandwidth=down_bandwidth,
|
||||
jitter=down_jitter,
|
||||
delay=down_delay,
|
||||
dup=down_duplicate,
|
||||
per=down_loss,
|
||||
unidirectional=True,
|
||||
)
|
||||
self.edge.asymmetric_link = core_pb2.Link(
|
||||
node_one_id=link.node_two_id,
|
||||
node_two_id=link.node_one_id,
|
||||
interface_one=asym_interface_one,
|
||||
interface_two=asym_interface_two,
|
||||
options=options,
|
||||
)
|
||||
else:
|
||||
link.options.unidirectional = False
|
||||
self.edge.asymmetric_link = None
|
||||
|
||||
if self.app.core.is_runtime() and link.HasField("options"):
|
||||
session_id = self.app.core.session_id
|
||||
self.app.core.client.edit_link(
|
||||
session_id,
|
||||
link.node_one_id,
|
||||
link.node_two_id,
|
||||
link.options,
|
||||
interface_one,
|
||||
interface_two,
|
||||
)
|
||||
if self.edge.asymmetric_link:
|
||||
self.app.core.client.edit_link(
|
||||
session_id,
|
||||
link.node_two_id,
|
||||
link.node_one_id,
|
||||
self.edge.asymmetric_link.options,
|
||||
interface_one,
|
||||
interface_two,
|
||||
)
|
||||
|
||||
self.destroy()
|
||||
|
||||
def change_symmetry(self):
|
||||
logging.debug("change symmetry")
|
||||
|
||||
if self.is_symmetric:
|
||||
self.is_symmetric = False
|
||||
self.symmetry_var.set("<<")
|
||||
if not self.asymmetric_frame:
|
||||
self.asymmetric_frame = self.get_frame()
|
||||
self.symmetric_frame.grid_forget()
|
||||
self.asymmetric_frame.grid(row=2, column=0)
|
||||
else:
|
||||
self.is_symmetric = True
|
||||
self.symmetry_var.set(">>")
|
||||
if not self.symmetric_frame:
|
||||
self.symmetric_frame = self.get_frame()
|
||||
self.asymmetric_frame.grid_forget()
|
||||
self.symmetric_frame.grid(row=2, column=0)
|
||||
|
||||
def load_link_config(self):
|
||||
"""
|
||||
populate link config to the table
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
width = self.app.canvas.itemcget(self.edge.id, "width")
|
||||
self.width.set(width)
|
||||
color = self.app.canvas.itemcget(self.edge.id, "fill")
|
||||
self.color.set(color)
|
||||
link = self.edge.link
|
||||
if link.HasField("options"):
|
||||
self.bandwidth.set(str(link.options.bandwidth))
|
||||
self.jitter.set(str(link.options.jitter))
|
||||
self.duplicate.set(str(link.options.dup))
|
||||
self.loss.set(str(link.options.per))
|
||||
self.delay.set(str(link.options.delay))
|
||||
if not self.is_symmetric:
|
||||
asym_link = self.edge.asymmetric_link
|
||||
self.down_bandwidth.set(str(asym_link.options.bandwidth))
|
||||
self.down_jitter.set(str(asym_link.options.jitter))
|
||||
self.down_duplicate.set(str(asym_link.options.dup))
|
||||
self.down_loss.set(str(asym_link.options.per))
|
||||
self.down_delay.set(str(asym_link.options.delay))
|
73
daemon/core/gui/dialogs/marker.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
"""
|
||||
marker dialog
|
||||
"""
|
||||
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.dialogs.colorpicker import ColorPicker
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
||||
MARKER_THICKNESS = [3, 5, 8, 10]
|
||||
|
||||
|
||||
class Marker(Dialog):
|
||||
def __init__(self, master, app, initcolor="#000000"):
|
||||
super().__init__(master, app, "marker tool", modal=False)
|
||||
self.app = app
|
||||
self.color = initcolor
|
||||
self.radius = MARKER_THICKNESS[0]
|
||||
self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0])
|
||||
self.draw()
|
||||
self.top.bind("<Destroy>", self.close_marker)
|
||||
|
||||
def draw(self):
|
||||
button = ttk.Button(self.top, text="clear", command=self.clear_marker)
|
||||
button.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=4)
|
||||
frame.grid(row=1, column=0, sticky="nsew")
|
||||
label = ttk.Label(frame, text="Thickness: ")
|
||||
label.grid(row=0, column=0, sticky="nsew")
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.marker_thickness,
|
||||
values=MARKER_THICKNESS,
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky="nsew")
|
||||
combobox.bind("<<ComboboxSelected>>", self.change_thickness)
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=4)
|
||||
frame.grid(row=2, column=0, sticky="nsew")
|
||||
label = ttk.Label(frame, text="Color: ")
|
||||
label.grid(row=0, column=0, sticky="nsew")
|
||||
label = ttk.Label(frame, background=self.color)
|
||||
label.grid(row=0, column=1, sticky="nsew")
|
||||
label.bind("<Button-1>", self.change_color)
|
||||
|
||||
def clear_marker(self):
|
||||
canvas = self.app.canvas
|
||||
for i in canvas.find_withtag("marker"):
|
||||
canvas.delete(i)
|
||||
|
||||
def change_color(self, event):
|
||||
color_picker = ColorPicker(self, self.app, self.color)
|
||||
color = color_picker.askcolor()
|
||||
event.widget.configure(background=color)
|
||||
self.color = color
|
||||
|
||||
def change_thickness(self, event):
|
||||
self.radius = self.marker_thickness.get()
|
||||
|
||||
def close_marker(self, event):
|
||||
logging.debug("destroy marker dialog")
|
||||
self.app.toolbar.marker_tool = None
|
||||
|
||||
def position(self):
|
||||
print(self.winfo_width(), self.winfo_height())
|
||||
self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y))
|
55
daemon/core/gui/dialogs/mobilityconfig.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
mobility configuration
|
||||
"""
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
||||
class MobilityConfigDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node):
|
||||
super().__init__(
|
||||
master,
|
||||
app,
|
||||
f"{canvas_node.core_node.name} Mobility Configuration",
|
||||
modal=True,
|
||||
)
|
||||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.config_frame = None
|
||||
try:
|
||||
self.config = self.app.core.get_mobility_config(self.node.id)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_apply_buttons()
|
||||
|
||||
def draw_apply_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
self.config_frame.parse_config()
|
||||
self.app.core.mobility_configs[self.node.id] = self.config
|
||||
self.destroy()
|
163
daemon/core/gui/dialogs/mobilityplayer.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.core_pb2 import MobilityAction
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
||||
ICON_SIZE = 16
|
||||
|
||||
|
||||
class MobilityPlayer:
|
||||
def __init__(self, master, app, canvas_node, config):
|
||||
self.master = master
|
||||
self.app = app
|
||||
self.canvas_node = canvas_node
|
||||
self.config = config
|
||||
self.dialog = None
|
||||
self.state = None
|
||||
|
||||
def show(self):
|
||||
if self.dialog:
|
||||
self.dialog.destroy()
|
||||
self.dialog = MobilityPlayerDialog(
|
||||
self.master, self.app, self.canvas_node, self.config
|
||||
)
|
||||
self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close)
|
||||
if self.state == MobilityAction.START:
|
||||
self.set_play()
|
||||
elif self.state == MobilityAction.PAUSE:
|
||||
self.set_pause()
|
||||
else:
|
||||
self.set_stop()
|
||||
self.dialog.show()
|
||||
|
||||
def handle_close(self):
|
||||
self.dialog.destroy()
|
||||
self.dialog = None
|
||||
|
||||
def set_play(self):
|
||||
self.state = MobilityAction.START
|
||||
if self.dialog:
|
||||
self.dialog.set_play()
|
||||
|
||||
def set_pause(self):
|
||||
self.state = MobilityAction.PAUSE
|
||||
if self.dialog:
|
||||
self.dialog.set_pause()
|
||||
|
||||
def set_stop(self):
|
||||
self.state = MobilityAction.STOP
|
||||
if self.dialog:
|
||||
self.dialog.set_stop()
|
||||
|
||||
|
||||
class MobilityPlayerDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node, config):
|
||||
super().__init__(
|
||||
master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False
|
||||
)
|
||||
self.resizable(False, False)
|
||||
self.geometry("")
|
||||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.config = config
|
||||
self.play_button = None
|
||||
self.pause_button = None
|
||||
self.stop_button = None
|
||||
self.progressbar = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
|
||||
file_name = self.config["file"].value
|
||||
label = ttk.Label(self.top, text=file_name)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
|
||||
self.progressbar = ttk.Progressbar(self.top, mode="indeterminate")
|
||||
self.progressbar.grid(sticky="ew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = Images.get(ImageEnum.START, width=ICON_SIZE)
|
||||
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
||||
self.play_button.image = image
|
||||
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE)
|
||||
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
||||
self.pause_button.image = image
|
||||
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
image = Images.get(ImageEnum.STOP, width=ICON_SIZE)
|
||||
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
||||
self.stop_button.image = image
|
||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
|
||||
loop = tk.IntVar(value=int(self.config["loop"].value == "1"))
|
||||
checkbutton = ttk.Checkbutton(
|
||||
frame, text="Loop?", variable=loop, state=tk.DISABLED
|
||||
)
|
||||
checkbutton.grid(row=0, column=3, padx=PADX)
|
||||
|
||||
rate = self.config["refresh_ms"].value
|
||||
label = ttk.Label(frame, text=f"rate {rate} ms")
|
||||
label.grid(row=0, column=4)
|
||||
|
||||
def clear_buttons(self):
|
||||
self.play_button.state(["!pressed"])
|
||||
self.pause_button.state(["!pressed"])
|
||||
self.stop_button.state(["!pressed"])
|
||||
|
||||
def set_play(self):
|
||||
self.clear_buttons()
|
||||
self.play_button.state(["pressed"])
|
||||
self.progressbar.start()
|
||||
|
||||
def set_pause(self):
|
||||
self.clear_buttons()
|
||||
self.pause_button.state(["pressed"])
|
||||
self.progressbar.stop()
|
||||
|
||||
def set_stop(self):
|
||||
self.clear_buttons()
|
||||
self.stop_button.state(["pressed"])
|
||||
self.progressbar.stop()
|
||||
|
||||
def click_play(self):
|
||||
self.set_play()
|
||||
session_id = self.app.core.session_id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.START
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def click_pause(self):
|
||||
self.set_pause()
|
||||
session_id = self.app.core.session_id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.PAUSE
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def click_stop(self):
|
||||
self.set_stop()
|
||||
session_id = self.app.core.session_id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.STOP
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
237
daemon/core/gui/dialogs/nodeconfig.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from functools import partial
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.dialogs.emaneconfig import EmaneModelDialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
|
||||
def mac_auto(is_auto, entry):
|
||||
logging.info("mac auto clicked")
|
||||
if is_auto.get():
|
||||
logging.info("disabling mac")
|
||||
entry.var.set("")
|
||||
entry.config(state=tk.DISABLED)
|
||||
else:
|
||||
entry.var.set("00:00:00:00:00:00")
|
||||
entry.config(state=tk.NORMAL)
|
||||
|
||||
|
||||
class InterfaceData:
|
||||
def __init__(self, is_auto, mac, ip4, ip6):
|
||||
self.is_auto = is_auto
|
||||
self.mac = mac
|
||||
self.ip4 = ip4
|
||||
self.ip6 = ip6
|
||||
|
||||
|
||||
class NodeConfigDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node):
|
||||
"""
|
||||
create an instance of node configuration
|
||||
|
||||
:param master: dialog master
|
||||
:param coretk.app.Application: main app
|
||||
:param coretk.graph.CanvasNode canvas_node: canvas node object
|
||||
"""
|
||||
super().__init__(
|
||||
master, app, f"{canvas_node.core_node.name} Configuration", modal=True
|
||||
)
|
||||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.image = canvas_node.image
|
||||
self.image_file = None
|
||||
self.image_button = None
|
||||
self.name = tk.StringVar(value=self.node.name)
|
||||
self.type = tk.StringVar(value=self.node.model)
|
||||
self.container_image = tk.StringVar(value=self.node.image)
|
||||
server = "localhost"
|
||||
if self.node.server:
|
||||
server = self.node.server
|
||||
self.server = tk.StringVar(value=server)
|
||||
self.interfaces = {}
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
row = 0
|
||||
|
||||
# field frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
# icon field
|
||||
label = ttk.Label(frame, text="Icon")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
self.image_button = ttk.Button(
|
||||
frame,
|
||||
text="Icon",
|
||||
image=self.image,
|
||||
compound=tk.NONE,
|
||||
command=self.click_icon,
|
||||
)
|
||||
self.image_button.grid(row=row, column=1, sticky="ew")
|
||||
row += 1
|
||||
|
||||
# name field
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.name,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.name, "%P"),
|
||||
)
|
||||
entry.bind(
|
||||
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "noname")
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew")
|
||||
row += 1
|
||||
|
||||
# node type field
|
||||
if NodeUtils.is_model_node(self.node.type):
|
||||
label = ttk.Label(frame, text="Type")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.type,
|
||||
values=list(NodeUtils.NODE_MODELS),
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky="ew")
|
||||
row += 1
|
||||
|
||||
# container image field
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
label = ttk.Label(frame, text="Image")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(frame, textvariable=self.container_image)
|
||||
entry.grid(row=row, column=1, sticky="ew")
|
||||
row += 1
|
||||
|
||||
if NodeUtils.is_container_node(self.node.type):
|
||||
# server
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Server")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
servers = ["localhost"]
|
||||
servers.extend(list(sorted(self.app.core.servers.keys())))
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.server, values=servers, state="readonly"
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky="ew")
|
||||
row += 1
|
||||
|
||||
# interfaces
|
||||
if self.canvas_node.interfaces:
|
||||
self.draw_interfaces()
|
||||
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_interfaces(self):
|
||||
notebook = ttk.Notebook(self.top)
|
||||
notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
|
||||
|
||||
for interface in self.canvas_node.interfaces:
|
||||
logging.info("interface: %s", interface)
|
||||
tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew", pady=PADY)
|
||||
tab.columnconfigure(1, weight=1)
|
||||
tab.columnconfigure(2, weight=1)
|
||||
notebook.add(tab, text=interface.name)
|
||||
|
||||
row = 0
|
||||
emane_node = self.canvas_node.has_emane_link(interface.id)
|
||||
if emane_node:
|
||||
emane_model = emane_node.emane.split("_")[1]
|
||||
button = ttk.Button(
|
||||
tab,
|
||||
text=f"Configure EMANE {emane_model}",
|
||||
command=lambda: self.click_emane_config(emane_model, interface.id),
|
||||
)
|
||||
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="MAC")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
is_auto = tk.BooleanVar(value=True)
|
||||
checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto)
|
||||
checkbutton.var = is_auto
|
||||
checkbutton.grid(row=row, column=1, padx=PADX)
|
||||
mac = tk.StringVar(value=interface.mac)
|
||||
entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED)
|
||||
entry.grid(row=row, column=2, sticky="ew")
|
||||
func = partial(mac_auto, is_auto, entry)
|
||||
checkbutton.config(command=func)
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="IPv4")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}")
|
||||
entry = ttk.Entry(tab, textvariable=ip4)
|
||||
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="IPv6")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}")
|
||||
entry = ttk.Entry(tab, textvariable=ip6)
|
||||
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
|
||||
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.config_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_emane_config(self, emane_model, interface_id):
|
||||
dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id)
|
||||
dialog.show()
|
||||
|
||||
def click_icon(self):
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
self.image_button.config(image=self.image)
|
||||
self.image_file = file_path
|
||||
|
||||
def config_apply(self):
|
||||
# update core node
|
||||
self.node.name = self.name.get()
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
self.node.image = self.container_image.get()
|
||||
server = self.server.get()
|
||||
if NodeUtils.is_container_node(self.node.type) and server != "localhost":
|
||||
self.node.server = server
|
||||
|
||||
# set custom icon
|
||||
if self.image_file:
|
||||
self.node.icon = self.image_file
|
||||
|
||||
# update canvas node
|
||||
self.canvas_node.image = self.image
|
||||
|
||||
# redraw
|
||||
self.canvas_node.redraw()
|
||||
self.destroy()
|
134
daemon/core/gui/dialogs/nodeservice.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
core node services
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.dialogs.serviceconfiguration import ServiceConfiguration
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll
|
||||
|
||||
|
||||
class NodeService(Dialog):
|
||||
def __init__(self, master, app, canvas_node, services=None):
|
||||
title = f"{canvas_node.core_node.name} Services"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.app = app
|
||||
self.canvas_node = canvas_node
|
||||
self.node_id = canvas_node.core_node.id
|
||||
self.groups = None
|
||||
self.services = None
|
||||
self.current = None
|
||||
if services is None:
|
||||
services = canvas_node.core_node.services
|
||||
model = canvas_node.core_node.model
|
||||
if len(services) == 0:
|
||||
services = set(self.app.core.default_services[model])
|
||||
else:
|
||||
services = set(services)
|
||||
|
||||
self.current_services = services
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="nsew", pady=PADY)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
for group in sorted(self.app.core.services):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Configure", command=self.click_configure)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event):
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.services[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
def service_clicked(self, name, var):
|
||||
if var.get() and name not in self.current_services:
|
||||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.current.listbox.delete(0, tk.END)
|
||||
for name in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, name)
|
||||
self.canvas_node.core_node.services[:] = self.current_services
|
||||
|
||||
def click_configure(self):
|
||||
current_selection = self.current.listbox.curselection()
|
||||
if len(current_selection):
|
||||
dialog = ServiceConfiguration(
|
||||
master=self,
|
||||
app=self.app,
|
||||
service_name=self.current.listbox.get(current_selection[0]),
|
||||
node_id=self.node_id,
|
||||
)
|
||||
dialog.show()
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Node service configuration", "Select a service to configure"
|
||||
)
|
||||
|
||||
def click_save(self):
|
||||
if (
|
||||
self.current_services
|
||||
!= self.app.core.default_services[self.canvas_node.core_node.model]
|
||||
):
|
||||
self.canvas_node.core_node.services[:] = self.current_services
|
||||
else:
|
||||
if len(self.canvas_node.core_node.services) > 0:
|
||||
self.canvas_node.core_node.services[:] = []
|
||||
self.destroy()
|
||||
|
||||
def click_cancel(self):
|
||||
self.current_services = None
|
||||
self.destroy()
|
143
daemon/core/gui/dialogs/observers.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.coreclient import Observer
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ListboxScroll
|
||||
|
||||
|
||||
class ObserverDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Observer Widgets", modal=True)
|
||||
self.observers = None
|
||||
self.save_button = None
|
||||
self.delete_button = None
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.name = tk.StringVar()
|
||||
self.cmd = tk.StringVar()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.draw_listbox()
|
||||
self.draw_form_fields()
|
||||
self.draw_config_buttons()
|
||||
self.draw_apply_buttons()
|
||||
|
||||
def draw_listbox(self):
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
listbox_scroll.columnconfigure(0, weight=1)
|
||||
listbox_scroll.rowconfigure(0, weight=1)
|
||||
self.observers = listbox_scroll.listbox
|
||||
self.observers.grid(row=0, column=0, sticky="nsew")
|
||||
self.observers.bind("<<ListboxSelect>>", self.handle_observer_change)
|
||||
for name in sorted(self.app.core.custom_observers):
|
||||
self.observers.insert(tk.END, name)
|
||||
|
||||
def draw_form_fields(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="Command")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.cmd)
|
||||
entry.grid(row=1, column=1, sticky="ew")
|
||||
|
||||
def draw_config_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
self.save_button = ttk.Button(
|
||||
frame, text="Save", state=tk.DISABLED, command=self.click_save
|
||||
)
|
||||
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_apply_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save_config)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_save_config(self):
|
||||
observers = []
|
||||
for name in sorted(self.app.core.custom_observers):
|
||||
observer = self.app.core.custom_observers[name]
|
||||
observers.append({"name": observer.name, "cmd": observer.cmd})
|
||||
self.app.guiconfig["observers"] = observers
|
||||
self.app.save_config()
|
||||
self.destroy()
|
||||
|
||||
def click_create(self):
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_observers:
|
||||
cmd = self.cmd.get()
|
||||
observer = Observer(name, cmd)
|
||||
self.app.core.custom_observers[name] = observer
|
||||
self.observers.insert(tk.END, name)
|
||||
|
||||
def click_save(self):
|
||||
name = self.name.get()
|
||||
if self.selected:
|
||||
previous_name = self.selected
|
||||
self.selected = name
|
||||
observer = self.app.core.custom_observers.pop(previous_name)
|
||||
observer.name = name
|
||||
observer.cmd = self.cmd.get()
|
||||
self.app.core.custom_observers[name] = observer
|
||||
self.observers.delete(self.selected_index)
|
||||
self.observers.insert(self.selected_index, name)
|
||||
self.observers.selection_set(self.selected_index)
|
||||
|
||||
def click_delete(self):
|
||||
if self.selected:
|
||||
self.observers.delete(self.selected_index)
|
||||
del self.app.core.custom_observers[self.selected]
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.name.set("")
|
||||
self.cmd.set("")
|
||||
self.observers.selection_clear(0, tk.END)
|
||||
self.save_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def handle_observer_change(self, event):
|
||||
selection = self.observers.curselection()
|
||||
if selection:
|
||||
self.selected_index = selection[0]
|
||||
self.selected = self.observers.get(self.selected_index)
|
||||
observer = self.app.core.custom_observers[self.selected]
|
||||
self.name.set(observer.name)
|
||||
self.cmd.set(observer.cmd)
|
||||
self.save_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.selected_index = None
|
||||
self.selected = None
|
||||
self.save_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
87
daemon/core/gui/dialogs/preferences.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui import appconfig
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
|
||||
class PreferencesDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Preferences", modal=True)
|
||||
preferences = self.app.guiconfig["preferences"]
|
||||
self.editor = tk.StringVar(value=preferences["editor"])
|
||||
self.theme = tk.StringVar(value=preferences["theme"])
|
||||
self.terminal = tk.StringVar(value=preferences["terminal"])
|
||||
self.gui3d = tk.StringVar(value=preferences["gui3d"])
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.draw_preferences()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_preferences(self):
|
||||
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Theme")
|
||||
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
themes = self.app.style.theme_names()
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.theme, values=themes, state="readonly"
|
||||
)
|
||||
combobox.set(self.theme.get())
|
||||
combobox.grid(row=0, column=1, sticky="ew")
|
||||
combobox.bind("<<ComboboxSelected>>", self.theme_change)
|
||||
|
||||
label = ttk.Label(frame, text="Editor")
|
||||
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly"
|
||||
)
|
||||
combobox.grid(row=1, column=1, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="Terminal")
|
||||
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.terminal,
|
||||
values=appconfig.TERMINALS,
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=2, column=1, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="3D GUI")
|
||||
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
||||
entry.grid(row=3, column=1, sticky="ew")
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def theme_change(self, event):
|
||||
theme = self.theme.get()
|
||||
logging.info("changing theme: %s", theme)
|
||||
self.app.style.theme_use(theme)
|
||||
|
||||
def click_save(self):
|
||||
preferences = self.app.guiconfig["preferences"]
|
||||
preferences["terminal"] = self.terminal.get()
|
||||
preferences["editor"] = self.editor.get()
|
||||
preferences["gui3d"] = self.gui3d.get()
|
||||
preferences["theme"] = self.theme.get()
|
||||
self.app.save_config()
|
||||
self.destroy()
|
173
daemon/core/gui/dialogs/servers.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from core.gui.coreclient import CoreServer
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import ListboxScroll
|
||||
|
||||
DEFAULT_NAME = "example"
|
||||
DEFAULT_ADDRESS = "127.0.0.1"
|
||||
DEFAULT_PORT = 50051
|
||||
|
||||
|
||||
class ServersDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "CORE Servers", modal=True)
|
||||
self.name = tk.StringVar(value=DEFAULT_NAME)
|
||||
self.address = tk.StringVar(value=DEFAULT_ADDRESS)
|
||||
self.port = tk.IntVar(value=DEFAULT_PORT)
|
||||
self.servers = None
|
||||
self.selected_index = None
|
||||
self.selected = None
|
||||
self.save_button = None
|
||||
self.delete_button = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.draw_servers()
|
||||
self.draw_servers_buttons()
|
||||
self.draw_server_configuration()
|
||||
self.draw_apply_buttons()
|
||||
|
||||
def draw_servers(self):
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(pady=PADY, sticky="nsew")
|
||||
listbox_scroll.columnconfigure(0, weight=1)
|
||||
listbox_scroll.rowconfigure(0, weight=1)
|
||||
|
||||
self.servers = listbox_scroll.listbox
|
||||
self.servers.grid(row=0, column=0, sticky="nsew")
|
||||
self.servers.bind("<<ListboxSelect>>", self.handle_server_change)
|
||||
|
||||
for server in self.app.core.servers:
|
||||
self.servers.insert(tk.END, server)
|
||||
|
||||
def draw_server_configuration(self):
|
||||
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.columnconfigure(5, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="Address")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(frame, textvariable=self.address)
|
||||
entry.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="Port")
|
||||
label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(
|
||||
frame,
|
||||
textvariable=self.port,
|
||||
validate="key",
|
||||
validatecommand=(self.app.validation.positive_int, "%P"),
|
||||
)
|
||||
entry.bind(
|
||||
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "50051")
|
||||
)
|
||||
entry.grid(row=0, column=5, sticky="ew")
|
||||
|
||||
def draw_servers_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
self.save_button = ttk.Button(
|
||||
frame, text="Save", state=tk.DISABLED, command=self.click_save
|
||||
)
|
||||
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_apply_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(
|
||||
frame, text="Save Configuration", command=self.click_save_configuration
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_save_configuration(self):
|
||||
servers = []
|
||||
for name in sorted(self.app.core.servers):
|
||||
server = self.app.core.servers[name]
|
||||
servers.append(
|
||||
{"name": server.name, "address": server.address, "port": server.port}
|
||||
)
|
||||
self.app.guiconfig["servers"] = servers
|
||||
self.app.save_config()
|
||||
self.destroy()
|
||||
|
||||
def click_create(self):
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.servers:
|
||||
address = self.address.get()
|
||||
port = self.port.get()
|
||||
server = CoreServer(name, address, port)
|
||||
self.app.core.servers[name] = server
|
||||
self.servers.insert(tk.END, name)
|
||||
|
||||
def click_save(self):
|
||||
name = self.name.get()
|
||||
if self.selected:
|
||||
previous_name = self.selected
|
||||
self.selected = name
|
||||
server = self.app.core.servers.pop(previous_name)
|
||||
server.name = name
|
||||
server.address = self.address.get()
|
||||
server.port = self.port.get()
|
||||
self.app.core.servers[name] = server
|
||||
self.servers.delete(self.selected_index)
|
||||
self.servers.insert(self.selected_index, name)
|
||||
self.servers.selection_set(self.selected_index)
|
||||
|
||||
def click_delete(self):
|
||||
if self.selected:
|
||||
self.servers.delete(self.selected_index)
|
||||
del self.app.core.servers[self.selected]
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.name.set(DEFAULT_NAME)
|
||||
self.address.set(DEFAULT_ADDRESS)
|
||||
self.port.set(DEFAULT_PORT)
|
||||
self.servers.selection_clear(0, tk.END)
|
||||
self.save_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def handle_server_change(self, event):
|
||||
selection = self.servers.curselection()
|
||||
if selection:
|
||||
self.selected_index = selection[0]
|
||||
self.selected = self.servers.get(self.selected_index)
|
||||
server = self.app.core.servers[self.selected]
|
||||
self.name.set(server.name)
|
||||
self.address.set(server.address)
|
||||
self.port.set(server.port)
|
||||
self.save_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.selected_index = None
|
||||
self.selected = None
|
||||
self.save_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
453
daemon/core/gui/dialogs/serviceconfiguration.py
Normal file
|
@ -0,0 +1,453 @@
|
|||
"Service configuration dialog"
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
|
||||
class ServiceConfiguration(Dialog):
|
||||
def __init__(self, master, app, service_name, node_id):
|
||||
title = f"{service_name} Service"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.app = app
|
||||
self.core = app.core
|
||||
self.node_id = node_id
|
||||
self.service_name = service_name
|
||||
self.radiovar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.metadata = ""
|
||||
self.filenames = []
|
||||
self.dependencies = []
|
||||
self.executables = []
|
||||
self.startup_commands = []
|
||||
self.validation_commands = []
|
||||
self.shutdown_commands = []
|
||||
self.validation_mode = None
|
||||
self.validation_time = None
|
||||
self.validation_period = None
|
||||
self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
||||
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16)
|
||||
|
||||
self.notebook = None
|
||||
self.metadata_entry = None
|
||||
self.filename_combobox = None
|
||||
self.startup_commands_listbox = None
|
||||
self.shutdown_commands_listbox = None
|
||||
self.validate_commands_listbox = None
|
||||
self.validation_time_entry = None
|
||||
self.validation_mode_entry = None
|
||||
self.service_file_data = None
|
||||
self.validation_period_entry = None
|
||||
self.original_service_files = {}
|
||||
self.temp_service_files = {}
|
||||
self.modified_files = set()
|
||||
self.load()
|
||||
self.draw()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
self.app.core.create_nodes_and_links()
|
||||
service_configs = self.app.core.service_configs
|
||||
if (
|
||||
self.node_id in service_configs
|
||||
and self.service_name in service_configs[self.node_id]
|
||||
):
|
||||
service_config = self.app.core.service_configs[self.node_id][
|
||||
self.service_name
|
||||
]
|
||||
else:
|
||||
service_config = self.app.core.get_node_service(
|
||||
self.node_id, self.service_name
|
||||
)
|
||||
self.dependencies = [x for x in service_config.dependencies]
|
||||
self.executables = [x for x in service_config.executables]
|
||||
self.metadata = service_config.meta
|
||||
self.filenames = [x for x in service_config.configs]
|
||||
self.startup_commands = [x for x in service_config.startup]
|
||||
self.validation_commands = [x for x in service_config.validate]
|
||||
self.shutdown_commands = [x for x in service_config.shutdown]
|
||||
self.validation_mode = service_config.validation_mode
|
||||
self.validation_time = service_config.validation_timer
|
||||
self.original_service_files = {
|
||||
x: self.app.core.get_node_service_file(
|
||||
self.node_id, self.service_name, x
|
||||
)
|
||||
for x in self.filenames
|
||||
}
|
||||
self.temp_service_files = {
|
||||
x: self.original_service_files[x] for x in self.original_service_files
|
||||
}
|
||||
file_configs = self.app.core.file_configs
|
||||
if (
|
||||
self.node_id in file_configs
|
||||
and self.service_name in file_configs[self.node_id]
|
||||
):
|
||||
for file, data in file_configs[self.node_id][self.service_name].items():
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
|
||||
# draw metadata
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Meta-data")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata)
|
||||
self.metadata_entry.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_tab_files()
|
||||
self.draw_tab_directories()
|
||||
self.draw_tab_startstop()
|
||||
self.draw_tab_configuration()
|
||||
|
||||
button = ttk.Button(self.top, text="Only Save Changes")
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_tab_files(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Files")
|
||||
|
||||
label = ttk.Label(
|
||||
tab, text="Config files and scripts that are generated for this service."
|
||||
)
|
||||
label.grid()
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="File Name")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||
self.filename_combobox = ttk.Combobox(
|
||||
frame, values=self.filenames, state="readonly"
|
||||
)
|
||||
self.filename_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.display_service_file_data
|
||||
)
|
||||
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, image=self.documentnew_img, state="disabled")
|
||||
button.bind("<Button-1>", self.add_filename)
|
||||
button.grid(row=0, column=2, padx=PADX)
|
||||
button = ttk.Button(frame, image=self.editdelete_img, state="disabled")
|
||||
button.bind("<Button-1>", self.delete_filename)
|
||||
button.grid(row=0, column=3)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Radiobutton(
|
||||
frame,
|
||||
variable=self.radiovar,
|
||||
text="Copy Source File",
|
||||
value=1,
|
||||
state=tk.DISABLED,
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = ttk.Entry(frame, state=tk.DISABLED)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
button = ttk.Radiobutton(
|
||||
frame,
|
||||
variable=self.radiovar,
|
||||
text="Use text below for file contents",
|
||||
value=2,
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1)
|
||||
image = Images.get(ImageEnum.DOCUMENTSAVE, 16)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
self.service_file_data = CodeText(tab)
|
||||
self.service_file_data.grid(sticky="nsew")
|
||||
tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1)
|
||||
if len(self.filenames) > 0:
|
||||
self.filename_combobox.current(0)
|
||||
self.service_file_data.text.delete(1.0, "end")
|
||||
self.service_file_data.text.insert(
|
||||
"end", self.temp_service_files[self.filenames[0]]
|
||||
)
|
||||
self.service_file_data.text.bind(
|
||||
"<FocusOut>", self.update_temp_service_file_data
|
||||
)
|
||||
|
||||
def draw_tab_directories(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Directories")
|
||||
|
||||
label = ttk.Label(
|
||||
tab,
|
||||
text="Directories required by this service that are unique for each node.",
|
||||
)
|
||||
label.grid()
|
||||
|
||||
def draw_tab_startstop(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
self.notebook.add(tab, text="Startup/Shutdown")
|
||||
|
||||
# tab 3
|
||||
for i in range(3):
|
||||
label_frame = None
|
||||
if i == 0:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Startup Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.startup_commands
|
||||
elif i == 1:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Shutdown Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.shutdown_commands
|
||||
elif i == 2:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Validation Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(1, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(row=0, column=0, sticky="nsew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=tk.StringVar())
|
||||
entry.grid(row=0, column=0, stick="ew", padx=PADX)
|
||||
button = ttk.Button(frame, image=self.documentnew_img)
|
||||
button.bind("<Button-1>", self.add_command)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, image=self.editdelete_img)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.bind("<Button-1>", self.delete_command)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.listbox.bind("<<ListboxSelect>>", self.update_entry)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(row=1, column=0, sticky="nsew")
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
self.shutdown_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 2:
|
||||
self.validate_commands_listbox = listbox_scroll.listbox
|
||||
|
||||
def draw_tab_configuration(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration", sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING:
|
||||
mode = "NON_BLOCKING"
|
||||
else:
|
||||
mode = "TIMER"
|
||||
self.validation_mode_entry = ttk.Entry(
|
||||
frame, textvariable=tk.StringVar(value=mode)
|
||||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=tk.StringVar()
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(
|
||||
frame, text="Defaults", command=self.click_defaults, state="disabled"
|
||||
)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(
|
||||
frame, text="Copy...", command=self.click_copy, state="disabled"
|
||||
)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def add_filename(self, event):
|
||||
# not worry about it for now
|
||||
return
|
||||
frame_contains_button = event.widget.master
|
||||
combobox = frame_contains_button.grid_slaves(row=0, column=1)[0]
|
||||
filename = combobox.get()
|
||||
if filename not in combobox["values"]:
|
||||
combobox["values"] += (filename,)
|
||||
|
||||
def delete_filename(self, event):
|
||||
# not worry about it for now
|
||||
return
|
||||
frame_comntains_button = event.widget.master
|
||||
combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0]
|
||||
filename = combobox.get()
|
||||
if filename in combobox["values"]:
|
||||
combobox["values"] = tuple([x for x in combobox["values"] if x != filename])
|
||||
combobox.set("")
|
||||
|
||||
def add_command(self, event):
|
||||
frame_contains_button = event.widget.master
|
||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
||||
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
|
||||
if command_to_add == "":
|
||||
return
|
||||
for cmd in listbox.get(0, tk.END):
|
||||
if cmd == command_to_add:
|
||||
return
|
||||
listbox.insert(tk.END, command_to_add)
|
||||
|
||||
def update_entry(self, event):
|
||||
listbox = event.widget
|
||||
current_selection = listbox.curselection()
|
||||
if len(current_selection) > 0:
|
||||
cmd = listbox.get(current_selection[0])
|
||||
entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves(
|
||||
row=0, column=0
|
||||
)[0]
|
||||
entry.delete(0, "end")
|
||||
entry.insert(0, cmd)
|
||||
|
||||
def delete_command(self, event):
|
||||
button = event.widget
|
||||
frame_contains_button = button.master
|
||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
||||
current_selection = listbox.curselection()
|
||||
if len(current_selection) > 0:
|
||||
listbox.delete(current_selection[0])
|
||||
entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
|
||||
entry.delete(0, tk.END)
|
||||
|
||||
def click_apply(self):
|
||||
service_configs = self.app.core.service_configs
|
||||
startup_commands = self.startup_commands_listbox.get(0, "end")
|
||||
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
|
||||
validate_commands = self.validate_commands_listbox.get(0, "end")
|
||||
try:
|
||||
config = self.core.set_node_service(
|
||||
self.node_id,
|
||||
self.service_name,
|
||||
startup_commands,
|
||||
validate_commands,
|
||||
shutdown_commands,
|
||||
)
|
||||
if self.node_id not in service_configs:
|
||||
service_configs[self.node_id] = {}
|
||||
if self.service_name not in service_configs[self.node_id]:
|
||||
self.app.core.service_configs[self.node_id][self.service_name] = config
|
||||
for file in self.modified_files:
|
||||
file_configs = self.app.core.file_configs
|
||||
if self.node_id not in file_configs:
|
||||
file_configs[self.node_id] = {}
|
||||
if self.service_name not in file_configs[self.node_id]:
|
||||
file_configs[self.node_id][self.service_name] = {}
|
||||
file_configs[self.node_id][self.service_name][
|
||||
file
|
||||
] = self.temp_service_files[file]
|
||||
|
||||
self.app.core.set_node_service_file(
|
||||
self.node_id, self.service_name, file, self.temp_service_files[file]
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
|
||||
def display_service_file_data(self, event):
|
||||
combobox = event.widget
|
||||
filename = combobox.get()
|
||||
self.service_file_data.text.delete(1.0, "end")
|
||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||
|
||||
def update_temp_service_file_data(self, event):
|
||||
scrolledtext = event.widget
|
||||
filename = self.filename_combobox.get()
|
||||
self.temp_service_files[filename] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[filename] != self.original_service_files[filename]:
|
||||
self.modified_files.add(filename)
|
||||
else:
|
||||
self.modified_files.discard(filename)
|
||||
|
||||
def click_defaults(self):
|
||||
logging.info("not implemented")
|
||||
|
||||
def click_copy(self):
|
||||
logging.info("not implemented")
|
||||
|
||||
def click_cancel(self):
|
||||
logging.info("not implemented")
|
53
daemon/core/gui/dialogs/sessionoptions.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import logging
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
||||
class SessionOptionsDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Session Options", modal=True)
|
||||
self.config_frame = None
|
||||
self.config = self.get_config()
|
||||
self.draw()
|
||||
|
||||
def get_config(self):
|
||||
try:
|
||||
session_id = self.app.core.session_id
|
||||
response = self.app.core.client.get_session_options(session_id)
|
||||
return response.config
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
self.config_frame = ConfigFrame(self.top, self.app, config=self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.save)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, padx=PADX, sticky="ew")
|
||||
|
||||
def save(self):
|
||||
config = self.config_frame.parse_config()
|
||||
try:
|
||||
session_id = self.app.core.session_id
|
||||
response = self.app.core.client.set_session_options(session_id, config)
|
||||
logging.info("saved session config: %s", response)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
181
daemon/core/gui/dialogs/sessions.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
import logging
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
||||
|
||||
class SessionsDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
super().__init__(master, app, "Sessions", modal=True)
|
||||
self.selected = False
|
||||
self.selected_id = None
|
||||
self.tree = None
|
||||
self.sessions = self.get_sessions()
|
||||
self.draw()
|
||||
|
||||
def get_sessions(self):
|
||||
try:
|
||||
response = self.app.core.client.get_sessions()
|
||||
logging.info("sessions: %s", response)
|
||||
return response.sessions
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
self.draw_description()
|
||||
self.draw_tree()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_description(self):
|
||||
"""
|
||||
write a short description
|
||||
:return: nothing
|
||||
"""
|
||||
label = ttk.Label(
|
||||
self.top,
|
||||
text="Below is a list of active CORE sessions. Double-click to \n"
|
||||
"connect to an existing session. Usually, only sessions in \n"
|
||||
"the RUNTIME state persist in the daemon, except for the \n"
|
||||
"one you might be concurrently editting.",
|
||||
justify=tk.CENTER,
|
||||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
def draw_tree(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame, columns=("id", "state", "nodes"), show="headings"
|
||||
)
|
||||
self.tree.grid(sticky="nsew")
|
||||
self.tree.column("id", stretch=tk.YES)
|
||||
self.tree.heading("id", text="ID")
|
||||
self.tree.column("state", stretch=tk.YES)
|
||||
self.tree.heading("state", text="State")
|
||||
self.tree.column("nodes", stretch=tk.YES)
|
||||
self.tree.heading("nodes", text="Node Count")
|
||||
|
||||
for index, session in enumerate(self.sessions):
|
||||
state_name = core_pb2.SessionState.Enum.Name(session.state)
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
text=str(session.id),
|
||||
values=(session.id, state_name, session.nodes),
|
||||
)
|
||||
self.tree.bind("<Double-1>", self.on_selected)
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
||||
b = ttk.Button(
|
||||
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, padx=PADX, sticky="ew")
|
||||
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
b = ttk.Button(
|
||||
frame,
|
||||
image=image,
|
||||
text="Connect",
|
||||
compound=tk.LEFT,
|
||||
command=self.click_connect,
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, column=1, padx=PADX, sticky="ew")
|
||||
|
||||
image = Images.get(ImageEnum.EDITDELETE, 16)
|
||||
b = ttk.Button(
|
||||
frame,
|
||||
image=image,
|
||||
text="Shutdown",
|
||||
compound=tk.LEFT,
|
||||
command=self.click_shutdown,
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, column=2, padx=PADX, sticky="ew")
|
||||
|
||||
b = ttk.Button(frame, text="Cancel", command=self.click_new)
|
||||
b.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_new(self):
|
||||
self.app.core.create_new_session()
|
||||
self.destroy()
|
||||
|
||||
def click_select(self, event):
|
||||
item = self.tree.selection()
|
||||
session_id = int(self.tree.item(item, "text"))
|
||||
self.selected = True
|
||||
self.selected_id = session_id
|
||||
|
||||
def click_connect(self):
|
||||
"""
|
||||
if no session is selected yet, create a new one else join that session
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.selected and self.selected_id is not None:
|
||||
self.join_session(self.selected_id)
|
||||
elif not self.selected and self.selected_id is None:
|
||||
self.click_new()
|
||||
else:
|
||||
logging.error("sessions invalid state")
|
||||
|
||||
def click_shutdown(self):
|
||||
"""
|
||||
if no session is currently selected create a new session else shut the selected
|
||||
session down.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.selected and self.selected_id is not None:
|
||||
self.shutdown_session(self.selected_id)
|
||||
elif not self.selected and self.selected_id is None:
|
||||
self.click_new()
|
||||
else:
|
||||
logging.error("querysessiondrawing.py invalid state")
|
||||
|
||||
def join_session(self, session_id):
|
||||
self.app.statusbar.progress_bar.start(5)
|
||||
thread = threading.Thread(
|
||||
target=self.app.core.join_session, args=([session_id])
|
||||
)
|
||||
thread.start()
|
||||
self.destroy()
|
||||
|
||||
def on_selected(self, event):
|
||||
item = self.tree.selection()
|
||||
sid = int(self.tree.item(item, "text"))
|
||||
self.join_session(sid)
|
||||
|
||||
def shutdown_session(self, sid):
|
||||
self.app.core.stop_session(sid)
|
||||
self.click_new()
|
||||
self.destroy()
|
252
daemon/core/gui/dialogs/shapemod.py
Normal file
|
@ -0,0 +1,252 @@
|
|||
"""
|
||||
shape input dialog
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import font, ttk
|
||||
|
||||
from core.gui.dialogs.colorpicker import ColorPicker
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.shapeutils import is_draw_shape, is_shape_text
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
|
||||
BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
|
||||
class ShapeDialog(Dialog):
|
||||
def __init__(self, master, app, shape):
|
||||
if is_draw_shape(shape.shape_type):
|
||||
title = "Add Shape"
|
||||
else:
|
||||
title = "Add Text"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.canvas = app.canvas
|
||||
self.fill = None
|
||||
self.border = None
|
||||
self.shape = shape
|
||||
data = shape.shape_data
|
||||
self.shape_text = tk.StringVar(value=data.text)
|
||||
self.font = tk.StringVar(value=data.font)
|
||||
self.font_size = tk.IntVar(value=data.font_size)
|
||||
self.text_color = data.text_color
|
||||
fill_color = data.fill_color
|
||||
if not fill_color:
|
||||
fill_color = "#CFCFFF"
|
||||
self.fill_color = fill_color
|
||||
self.border_color = data.border_color
|
||||
self.border_width = tk.IntVar(value=0)
|
||||
self.bold = tk.BooleanVar(value=data.bold)
|
||||
self.italic = tk.BooleanVar(value=data.italic)
|
||||
self.underline = tk.BooleanVar(value=data.underline)
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.draw_label_options()
|
||||
if is_draw_shape(self.shape.shape_type):
|
||||
self.draw_shape_options()
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_label_options(self):
|
||||
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
entry = ttk.Entry(label_frame, textvariable=self.shape_text)
|
||||
entry.grid(sticky="ew", pady=PADY)
|
||||
|
||||
# font options
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.font,
|
||||
values=sorted(font.families()),
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=0, column=0, sticky="nsew")
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=1, padx=PADX, sticky="nsew")
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_text_color)
|
||||
button.grid(row=0, column=2, sticky="nsew")
|
||||
|
||||
# style options
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Checkbutton(frame, variable=self.bold, text="Bold")
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
button = ttk.Checkbutton(frame, variable=self.italic, text="Italic")
|
||||
button.grid(row=0, column=1, padx=PADX, sticky="ew")
|
||||
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_shape_options(self):
|
||||
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(1, 3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label = ttk.Label(frame, text="Fill Color")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||
self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color)
|
||||
self.fill.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_fill_color)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="Border Color")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
self.border = ttk.Label(
|
||||
frame, text=self.border_color, background=self.border_color
|
||||
)
|
||||
self.border.grid(row=1, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_border_color)
|
||||
button.grid(row=1, column=2, sticky="ew")
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Border Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky="nsew")
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="nsew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Add shape", command=self.click_add)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.cancel)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def choose_text_color(self):
|
||||
color_picker = ColorPicker(self, self.app, "#000000")
|
||||
color = color_picker.askcolor()
|
||||
self.text_color = color
|
||||
|
||||
def choose_fill_color(self):
|
||||
color_picker = ColorPicker(self, self.app, self.fill_color)
|
||||
color = color_picker.askcolor()
|
||||
self.fill_color = color
|
||||
self.fill.config(background=color, text=color)
|
||||
|
||||
def choose_border_color(self):
|
||||
color_picker = ColorPicker(self, self.app, self.border_color)
|
||||
color = color_picker.askcolor()
|
||||
self.border_color = color
|
||||
self.border.config(background=color, text=color)
|
||||
|
||||
def cancel(self):
|
||||
self.shape.delete()
|
||||
self.canvas.shapes.pop(self.shape.id)
|
||||
self.destroy()
|
||||
|
||||
def click_add(self):
|
||||
if is_draw_shape(self.shape.shape_type):
|
||||
self.add_shape()
|
||||
elif is_shape_text(self.shape.shape_type):
|
||||
self.add_text()
|
||||
self.destroy()
|
||||
|
||||
def make_font(self):
|
||||
"""
|
||||
create font for text or shape label
|
||||
:return: list(font specifications)
|
||||
"""
|
||||
size = int(self.font_size.get())
|
||||
text_font = [self.font.get(), size]
|
||||
if self.bold.get():
|
||||
text_font.append("bold")
|
||||
if self.italic.get():
|
||||
text_font.append("italic")
|
||||
if self.underline.get():
|
||||
text_font.append("underline")
|
||||
return text_font
|
||||
|
||||
def save_text(self):
|
||||
"""
|
||||
save info related to text or shape label
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
data = self.shape.shape_data
|
||||
data.text = self.shape_text.get()
|
||||
data.font = self.font.get()
|
||||
data.font_size = int(self.font_size.get())
|
||||
data.text_color = self.text_color
|
||||
data.bold = self.bold.get()
|
||||
data.italic = self.italic.get()
|
||||
data.underline = self.underline.get()
|
||||
|
||||
def save_shape(self):
|
||||
"""
|
||||
save info related to shape
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
data = self.shape.shape_data
|
||||
data.fill_color = self.fill_color
|
||||
data.border_color = self.border_color
|
||||
data.border_width = int(self.border_width.get())
|
||||
|
||||
def add_text(self):
|
||||
"""
|
||||
add text to canvas
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
text = self.shape_text.get()
|
||||
text_font = self.make_font()
|
||||
self.canvas.itemconfig(
|
||||
self.shape.id, text=text, fill=self.text_color, font=text_font
|
||||
)
|
||||
self.save_text()
|
||||
|
||||
def add_shape(self):
|
||||
self.canvas.itemconfig(
|
||||
self.shape.id,
|
||||
fill=self.fill_color,
|
||||
dash="",
|
||||
outline=self.border_color,
|
||||
width=int(self.border_width.get()),
|
||||
)
|
||||
shape_text = self.shape_text.get()
|
||||
size = int(self.font_size.get())
|
||||
x0, y0, x1, y1 = self.canvas.bbox(self.shape.id)
|
||||
_y = y0 + 1.5 * size
|
||||
_x = (x0 + x1) / 2
|
||||
text_font = self.make_font()
|
||||
if self.shape.text_id is None:
|
||||
self.shape.text_id = self.canvas.create_text(
|
||||
_x,
|
||||
_y,
|
||||
text=shape_text,
|
||||
fill=self.text_color,
|
||||
font=text_font,
|
||||
tags=tags.SHAPE_TEXT,
|
||||
)
|
||||
self.shape.created = True
|
||||
else:
|
||||
self.canvas.itemconfig(
|
||||
self.shape.text_id,
|
||||
text=shape_text,
|
||||
fill=self.text_color,
|
||||
font=text_font,
|
||||
)
|
||||
self.save_text()
|
||||
self.save_shape()
|
66
daemon/core/gui/dialogs/wlanconfig.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
wlan configuration
|
||||
"""
|
||||
|
||||
from tkinter import ttk
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
||||
class WlanConfigDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node):
|
||||
super().__init__(
|
||||
master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True
|
||||
)
|
||||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.config_frame = None
|
||||
try:
|
||||
self.config = self.app.core.get_wlan_config(self.node.id)
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_apply_buttons()
|
||||
|
||||
def draw_apply_buttons(self):
|
||||
"""
|
||||
create node configuration options
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
"""
|
||||
retrieve user's wlan configuration and store the new configuration values
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
config = self.config_frame.parse_config()
|
||||
self.app.core.wlan_configs[self.node.id] = self.config
|
||||
if self.app.core.is_runtime():
|
||||
session_id = self.app.core.session_id
|
||||
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
|
||||
self.destroy()
|
8
daemon/core/gui/errors.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from tkinter import messagebox
|
||||
|
||||
|
||||
def show_grpc_error(e):
|
||||
title = [x.capitalize() for x in e.code().name.lower().split("_")]
|
||||
title = " ".join(title)
|
||||
title = f"GRPC {title}"
|
||||
messagebox.showerror(title, e.details())
|
0
daemon/core/gui/graph/__init__.py
Normal file
181
daemon/core/gui/graph/edges.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter.font import Font
|
||||
|
||||
from core.gui import themes
|
||||
from core.gui.dialogs.linkconfig import LinkConfiguration
|
||||
from core.gui.graph import tags
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
|
||||
TEXT_DISTANCE = 0.30
|
||||
|
||||
|
||||
class CanvasWirelessEdge:
|
||||
def __init__(self, token, position, src, dst, canvas):
|
||||
self.token = token
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
self.canvas = canvas
|
||||
self.id = self.canvas.create_line(
|
||||
*position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933"
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
self.canvas.delete(self.id)
|
||||
|
||||
|
||||
class CanvasEdge:
|
||||
"""
|
||||
Canvas edge class
|
||||
"""
|
||||
|
||||
width = 3
|
||||
|
||||
def __init__(self, x1, y1, x2, y2, src, canvas):
|
||||
"""
|
||||
Create an instance of canvas edge object
|
||||
:param int x1: source x-coord
|
||||
:param int y1: source y-coord
|
||||
:param int x2: destination x-coord
|
||||
:param int y2: destination y-coord
|
||||
:param int src: source id
|
||||
:param coretk.graph.graph.GraphCanvas canvas: canvas object
|
||||
"""
|
||||
self.src = src
|
||||
self.dst = None
|
||||
self.src_interface = None
|
||||
self.dst_interface = None
|
||||
self.canvas = canvas
|
||||
self.id = self.canvas.create_line(
|
||||
x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000"
|
||||
)
|
||||
self.text_src = None
|
||||
self.text_dst = None
|
||||
self.token = None
|
||||
self.font = Font(size=8)
|
||||
self.link = None
|
||||
self.asymmetric_link = None
|
||||
self.throughput = None
|
||||
self.set_binding()
|
||||
|
||||
def set_binding(self):
|
||||
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.create_context)
|
||||
|
||||
def set_link(self, link):
|
||||
self.link = link
|
||||
self.draw_labels()
|
||||
|
||||
def get_coordinates(self):
|
||||
x1, y1, x2, y2 = self.canvas.coords(self.id)
|
||||
v1 = x2 - x1
|
||||
v2 = y2 - y1
|
||||
ux = TEXT_DISTANCE * v1
|
||||
uy = TEXT_DISTANCE * v2
|
||||
x1 = x1 + ux
|
||||
y1 = y1 + uy
|
||||
x2 = x2 - ux
|
||||
y2 = y2 - uy
|
||||
return x1, y1, x2, y2
|
||||
|
||||
def draw_labels(self):
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
label_one = None
|
||||
if self.link.HasField("interface_one"):
|
||||
label_one = (
|
||||
f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n"
|
||||
f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n"
|
||||
)
|
||||
label_two = None
|
||||
if self.link.HasField("interface_two"):
|
||||
label_two = (
|
||||
f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n"
|
||||
f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n"
|
||||
)
|
||||
self.text_src = self.canvas.create_text(
|
||||
x1,
|
||||
y1,
|
||||
text=label_one,
|
||||
justify=tk.CENTER,
|
||||
font=self.font,
|
||||
tags=tags.LINK_INFO,
|
||||
)
|
||||
self.text_dst = self.canvas.create_text(
|
||||
x2,
|
||||
y2,
|
||||
text=label_two,
|
||||
justify=tk.CENTER,
|
||||
font=self.font,
|
||||
tags=tags.LINK_INFO,
|
||||
)
|
||||
|
||||
def update_labels(self):
|
||||
"""
|
||||
Move edge labels based on current position.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
self.canvas.coords(self.text_src, x1, y1)
|
||||
self.canvas.coords(self.text_dst, x2, y2)
|
||||
|
||||
def complete(self, dst):
|
||||
self.dst = dst
|
||||
self.token = tuple(sorted((self.src, self.dst)))
|
||||
x, y = self.canvas.coords(self.dst)
|
||||
x1, y1, _, _ = self.canvas.coords(self.id)
|
||||
self.canvas.coords(self.id, x1, y1, x, y)
|
||||
self.check_wireless()
|
||||
self.canvas.tag_raise(self.src)
|
||||
self.canvas.tag_raise(self.dst)
|
||||
|
||||
def check_wireless(self):
|
||||
src_node = self.canvas.nodes[self.src]
|
||||
dst_node = self.canvas.nodes[self.dst]
|
||||
src_node_type = src_node.core_node.type
|
||||
dst_node_type = dst_node.core_node.type
|
||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||
if is_src_wireless or is_dst_wireless:
|
||||
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
|
||||
self._check_antenna()
|
||||
|
||||
def _check_antenna(self):
|
||||
src_node = self.canvas.nodes[self.src]
|
||||
dst_node = self.canvas.nodes[self.dst]
|
||||
src_node_type = src_node.core_node.type
|
||||
dst_node_type = dst_node.core_node.type
|
||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||
if is_src_wireless or is_dst_wireless:
|
||||
if is_src_wireless and not is_dst_wireless:
|
||||
dst_node.add_antenna()
|
||||
elif not is_src_wireless and is_dst_wireless:
|
||||
src_node.add_antenna()
|
||||
# TODO: remove this? dont allow linking wireless nodes?
|
||||
else:
|
||||
src_node.add_antenna()
|
||||
|
||||
def delete(self):
|
||||
self.canvas.delete(self.id)
|
||||
if self.link:
|
||||
self.canvas.delete(self.text_src)
|
||||
self.canvas.delete(self.text_dst)
|
||||
|
||||
def create_context(self, event):
|
||||
logging.debug("create link context")
|
||||
context = tk.Menu(self.canvas)
|
||||
themes.style_menu(context)
|
||||
context.add_command(label="Configure", command=self.configure)
|
||||
context.add_command(label="Delete")
|
||||
context.add_command(label="Split")
|
||||
context.add_command(label="Merge")
|
||||
if self.canvas.app.core.is_runtime():
|
||||
context.entryconfigure(1, state="disabled")
|
||||
context.entryconfigure(2, state="disabled")
|
||||
context.entryconfigure(3, state="disabled")
|
||||
context.post(event.x_root, event.y_root)
|
||||
|
||||
def configure(self):
|
||||
logging.debug("link configuration")
|
||||
dialog = LinkConfiguration(self.canvas, self.canvas.app, self)
|
||||
dialog.show()
|
18
daemon/core/gui/graph/enums.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import enum
|
||||
|
||||
|
||||
class GraphMode(enum.Enum):
|
||||
SELECT = 0
|
||||
EDGE = 1
|
||||
PICKNODE = 2
|
||||
NODE = 3
|
||||
ANNOTATION = 4
|
||||
OTHER = 5
|
||||
|
||||
|
||||
class ScaleOption(enum.Enum):
|
||||
NONE = 0
|
||||
UPPER_LEFT = 1
|
||||
CENTERED = 2
|
||||
SCALED = 3
|
||||
TILED = 4
|
839
daemon/core/gui/graph/graph.py
Normal file
|
@ -0,0 +1,839 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui import nodeutils
|
||||
from core.gui.dialogs.shapemod import ShapeDialog
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
|
||||
from core.gui.graph.enums import GraphMode, ScaleOption
|
||||
from core.gui.graph.linkinfo import Throughput
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.shape import Shape
|
||||
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
|
||||
ZOOM_IN = 1.1
|
||||
ZOOM_OUT = 0.9
|
||||
|
||||
|
||||
class CanvasGraph(tk.Canvas):
|
||||
def __init__(self, master, core, width, height):
|
||||
super().__init__(master, highlightthickness=0, background="#cccccc")
|
||||
self.app = master
|
||||
self.core = core
|
||||
self.mode = GraphMode.SELECT
|
||||
self.annotation_type = None
|
||||
self.selection = {}
|
||||
self.select_box = None
|
||||
self.selected = None
|
||||
self.node_draw = None
|
||||
self.context = None
|
||||
self.nodes = {}
|
||||
self.edges = {}
|
||||
self.shapes = {}
|
||||
self.wireless_edges = {}
|
||||
self.drawing_edge = None
|
||||
self.grid = None
|
||||
self.throughput_draw = Throughput(self, core)
|
||||
self.shape_drawing = False
|
||||
self.default_dimensions = (width, height)
|
||||
self.current_dimensions = self.default_dimensions
|
||||
self.ratio = 1.0
|
||||
self.offset = (0, 0)
|
||||
self.cursor = (0, 0)
|
||||
self.marker_tool = None
|
||||
|
||||
# background related
|
||||
self.wallpaper_id = None
|
||||
self.wallpaper = None
|
||||
self.wallpaper_drawn = None
|
||||
self.wallpaper_file = ""
|
||||
self.scale_option = tk.IntVar(value=1)
|
||||
self.show_grid = tk.BooleanVar(value=True)
|
||||
self.adjust_to_dim = tk.BooleanVar(value=False)
|
||||
|
||||
# bindings
|
||||
self.setup_bindings()
|
||||
|
||||
# draw base canvas
|
||||
self.draw_canvas()
|
||||
self.draw_grid()
|
||||
|
||||
def draw_canvas(self, dimensions=None):
|
||||
if self.grid is not None:
|
||||
self.delete(self.grid)
|
||||
if not dimensions:
|
||||
dimensions = self.default_dimensions
|
||||
self.current_dimensions = dimensions
|
||||
self.grid = self.create_rectangle(
|
||||
0,
|
||||
0,
|
||||
*dimensions,
|
||||
outline="#000000",
|
||||
fill="#ffffff",
|
||||
width=1,
|
||||
tags="rectangle",
|
||||
)
|
||||
self.configure(scrollregion=self.bbox(tk.ALL))
|
||||
|
||||
def reset_and_redraw(self, session):
|
||||
"""
|
||||
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
|
||||
client.
|
||||
|
||||
:param core.api.grpc.core_pb2.Session session: session to draw
|
||||
:return: nothing
|
||||
"""
|
||||
# hide context
|
||||
self.hide_context()
|
||||
|
||||
# delete any existing drawn items
|
||||
for tag in tags.COMPONENT_TAGS:
|
||||
self.delete(tag)
|
||||
|
||||
# set the private variables to default value
|
||||
self.mode = GraphMode.SELECT
|
||||
self.annotation_type = None
|
||||
self.node_draw = None
|
||||
self.selected = None
|
||||
self.nodes.clear()
|
||||
self.edges.clear()
|
||||
self.shapes.clear()
|
||||
self.wireless_edges.clear()
|
||||
self.drawing_edge = None
|
||||
self.draw_session(session)
|
||||
|
||||
def setup_bindings(self):
|
||||
"""
|
||||
Bind any mouse events or hot keys to the matching action
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.bind("<ButtonPress-1>", self.click_press)
|
||||
self.bind("<ButtonRelease-1>", self.click_release)
|
||||
self.bind("<B1-Motion>", self.click_motion)
|
||||
self.bind("<ButtonRelease-3>", self.click_context)
|
||||
self.bind("<Delete>", self.press_delete)
|
||||
self.bind("<Control-1>", self.ctrl_click)
|
||||
self.bind("<Double-Button-1>", self.double_click)
|
||||
self.bind("<MouseWheel>", self.zoom)
|
||||
self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN))
|
||||
self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT))
|
||||
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
|
||||
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
|
||||
|
||||
def hide_context(self):
|
||||
if self.context:
|
||||
self.context.unpost()
|
||||
self.context = None
|
||||
|
||||
def get_actual_coords(self, x, y):
|
||||
actual_x = (x - self.offset[0]) / self.ratio
|
||||
actual_y = (y - self.offset[1]) / self.ratio
|
||||
return actual_x, actual_y
|
||||
|
||||
def get_scaled_coords(self, x, y):
|
||||
scaled_x = (x * self.ratio) + self.offset[0]
|
||||
scaled_y = (y * self.ratio) + self.offset[1]
|
||||
return scaled_x, scaled_y
|
||||
|
||||
def inside_canvas(self, x, y):
|
||||
x1, y1, x2, y2 = self.bbox(self.grid)
|
||||
valid_x = x1 <= x <= x2
|
||||
valid_y = y1 <= y <= y2
|
||||
return valid_x and valid_y
|
||||
|
||||
def valid_position(self, x1, y1, x2, y2):
|
||||
valid_topleft = self.inside_canvas(x1, y1)
|
||||
valid_bottomright = self.inside_canvas(x2, y2)
|
||||
return valid_topleft and valid_bottomright
|
||||
|
||||
def draw_grid(self):
|
||||
"""
|
||||
Create grid.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
width, height = self.width_and_height()
|
||||
width = int(width)
|
||||
height = int(height)
|
||||
for i in range(0, width, 27):
|
||||
self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE)
|
||||
for i in range(0, height, 27):
|
||||
self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
|
||||
self.tag_lower(tags.GRIDLINE)
|
||||
self.tag_lower(self.grid)
|
||||
|
||||
def add_wireless_edge(self, src, dst):
|
||||
"""
|
||||
add a wireless edge between 2 canvas nodes
|
||||
|
||||
:param CanvasNode src: source node
|
||||
:param CanvasNode dst: destination node
|
||||
:return: nothing
|
||||
"""
|
||||
token = tuple(sorted((src.id, dst.id)))
|
||||
x1, y1 = self.coords(src.id)
|
||||
x2, y2 = self.coords(dst.id)
|
||||
position = (x1, y1, x2, y2)
|
||||
edge = CanvasWirelessEdge(token, position, src.id, dst.id, self)
|
||||
self.wireless_edges[token] = edge
|
||||
src.wireless_edges.add(edge)
|
||||
dst.wireless_edges.add(edge)
|
||||
self.tag_raise(src.id)
|
||||
self.tag_raise(dst.id)
|
||||
|
||||
def delete_wireless_edge(self, src, dst):
|
||||
token = tuple(sorted((src.id, dst.id)))
|
||||
edge = self.wireless_edges.pop(token)
|
||||
edge.delete()
|
||||
src.wireless_edges.remove(edge)
|
||||
dst.wireless_edges.remove(edge)
|
||||
|
||||
def draw_session(self, session):
|
||||
"""
|
||||
Draw existing session.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# draw existing nodes
|
||||
for core_node in session.nodes:
|
||||
# peer to peer node is not drawn on the GUI
|
||||
if NodeUtils.is_ignore_node(core_node.type):
|
||||
continue
|
||||
|
||||
# draw nodes on the canvas
|
||||
logging.info("drawing core node: %s", core_node)
|
||||
image = NodeUtils.node_icon(core_node.type, core_node.model)
|
||||
if core_node.icon:
|
||||
try:
|
||||
image = Images.create(core_node.icon, nodeutils.ICON_SIZE)
|
||||
except OSError:
|
||||
logging.error("invalid icon: %s", core_node.icon)
|
||||
|
||||
x = core_node.position.x
|
||||
y = core_node.position.y
|
||||
node = CanvasNode(self.master, x, y, core_node, image)
|
||||
self.nodes[node.id] = node
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
|
||||
# draw existing links
|
||||
for link in session.links:
|
||||
logging.info("drawing link: %s", link)
|
||||
canvas_node_one = self.core.canvas_nodes[link.node_one_id]
|
||||
node_one = canvas_node_one.core_node
|
||||
canvas_node_two = self.core.canvas_nodes[link.node_two_id]
|
||||
node_two = canvas_node_two.core_node
|
||||
token = tuple(sorted((canvas_node_one.id, canvas_node_two.id)))
|
||||
|
||||
if link.type == core_pb2.LinkType.WIRELESS:
|
||||
self.add_wireless_edge(canvas_node_one, canvas_node_two)
|
||||
else:
|
||||
if token not in self.edges:
|
||||
edge = CanvasEdge(
|
||||
node_one.position.x,
|
||||
node_one.position.y,
|
||||
node_two.position.x,
|
||||
node_two.position.y,
|
||||
canvas_node_one.id,
|
||||
self,
|
||||
)
|
||||
edge.token = token
|
||||
edge.dst = canvas_node_two.id
|
||||
edge.set_link(link)
|
||||
edge.check_wireless()
|
||||
canvas_node_one.edges.add(edge)
|
||||
canvas_node_two.edges.add(edge)
|
||||
self.edges[edge.token] = edge
|
||||
self.core.links[edge.token] = edge
|
||||
if link.HasField("interface_one"):
|
||||
canvas_node_one.interfaces.append(link.interface_one)
|
||||
if link.HasField("interface_two"):
|
||||
canvas_node_two.interfaces.append(link.interface_two)
|
||||
elif link.options.unidirectional:
|
||||
edge = self.edges[token]
|
||||
edge.asymmetric_link = link
|
||||
else:
|
||||
logging.error("duplicate link received: %s", link)
|
||||
|
||||
# raise the nodes so they on top of the links
|
||||
self.tag_raise(tags.NODE)
|
||||
|
||||
def canvas_xy(self, event):
|
||||
"""
|
||||
Convert window coordinate to canvas coordinate
|
||||
|
||||
:param event:
|
||||
:rtype: (int, int)
|
||||
:return: x, y canvas coordinate
|
||||
"""
|
||||
x = self.canvasx(event.x)
|
||||
y = self.canvasy(event.y)
|
||||
return x, y
|
||||
|
||||
def get_selected(self, event):
|
||||
"""
|
||||
Retrieve the item id that is on the mouse position
|
||||
|
||||
:param event: mouse event
|
||||
:rtype: int
|
||||
:return: the item that the mouse point to
|
||||
"""
|
||||
x, y = self.canvas_xy(event)
|
||||
overlapping = self.find_overlapping(x, y, x, y)
|
||||
selected = None
|
||||
for _id in overlapping:
|
||||
if self.drawing_edge and self.drawing_edge.id == _id:
|
||||
continue
|
||||
|
||||
if _id in self.nodes:
|
||||
selected = _id
|
||||
break
|
||||
|
||||
if _id in self.shapes:
|
||||
selected = _id
|
||||
|
||||
return selected
|
||||
|
||||
def click_release(self, event):
|
||||
"""
|
||||
Draw a node or finish drawing an edge according to the current graph mode
|
||||
|
||||
:param event: mouse event
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("click release")
|
||||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
return
|
||||
|
||||
if self.context:
|
||||
self.hide_context()
|
||||
else:
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
self.focus_set()
|
||||
if self.shape_drawing:
|
||||
shape = self.shapes[self.selected]
|
||||
shape.shape_complete(x, y)
|
||||
self.shape_drawing = False
|
||||
elif self.mode == GraphMode.SELECT:
|
||||
self.focus_set()
|
||||
if self.select_box:
|
||||
x0, y0, x1, y1 = self.coords(self.select_box.id)
|
||||
inside = [
|
||||
x
|
||||
for x in self.find_enclosed(x0, y0, x1, y1)
|
||||
if "node" in self.gettags(x) or "shape" in self.gettags(x)
|
||||
]
|
||||
for i in inside:
|
||||
self.select_object(i, True)
|
||||
self.select_box.disappear()
|
||||
self.select_box = None
|
||||
else:
|
||||
self.focus_set()
|
||||
self.selected = self.get_selected(event)
|
||||
logging.debug(
|
||||
f"click release selected({self.selected}) mode({self.mode})"
|
||||
)
|
||||
if self.mode == GraphMode.EDGE:
|
||||
self.handle_edge_release(event)
|
||||
elif self.mode == GraphMode.NODE:
|
||||
self.add_node(x, y)
|
||||
elif self.mode == GraphMode.PICKNODE:
|
||||
self.mode = GraphMode.NODE
|
||||
self.selected = None
|
||||
|
||||
def handle_edge_release(self, event):
|
||||
edge = self.drawing_edge
|
||||
self.drawing_edge = None
|
||||
|
||||
# not drawing edge return
|
||||
if edge is None:
|
||||
return
|
||||
|
||||
# edge dst must be a node
|
||||
logging.debug(f"current selected: {self.selected}")
|
||||
dst_node = self.nodes.get(self.selected)
|
||||
if not dst_node:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# edge dst is same as src, delete edge
|
||||
if edge.src == self.selected:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# ignore repeated edges
|
||||
token = tuple(sorted((edge.src, self.selected)))
|
||||
if token in self.edges:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# set dst node and snap edge to center
|
||||
edge.complete(self.selected)
|
||||
logging.debug("drawing edge token: %s", edge.token)
|
||||
|
||||
self.edges[edge.token] = edge
|
||||
node_src = self.nodes[edge.src]
|
||||
node_src.edges.add(edge)
|
||||
node_dst = self.nodes[edge.dst]
|
||||
node_dst.edges.add(edge)
|
||||
self.core.create_link(edge, node_src, node_dst)
|
||||
|
||||
def select_object(self, object_id, choose_multiple=False):
|
||||
"""
|
||||
create a bounding box when a node is selected
|
||||
"""
|
||||
if not choose_multiple:
|
||||
self.clear_selection()
|
||||
|
||||
# draw a bounding box if node hasn't been selected yet
|
||||
if object_id not in self.selection:
|
||||
x0, y0, x1, y1 = self.bbox(object_id)
|
||||
selection_id = self.create_rectangle(
|
||||
(x0 - 6, y0 - 6, x1 + 6, y1 + 6),
|
||||
activedash=True,
|
||||
dash="-",
|
||||
tags=tags.SELECTION,
|
||||
)
|
||||
self.selection[object_id] = selection_id
|
||||
else:
|
||||
selection_id = self.selection.pop(object_id)
|
||||
self.delete(selection_id)
|
||||
|
||||
def clear_selection(self):
|
||||
"""
|
||||
Clear current selection boxes.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
for _id in self.selection.values():
|
||||
self.delete(_id)
|
||||
self.selection.clear()
|
||||
|
||||
def move_selection(self, object_id, x_offset, y_offset):
|
||||
select_id = self.selection.get(object_id)
|
||||
if select_id is not None:
|
||||
self.move(select_id, x_offset, y_offset)
|
||||
|
||||
def delete_selection_objects(self):
|
||||
edges = set()
|
||||
nodes = []
|
||||
for object_id in self.selection:
|
||||
# delete selection box
|
||||
selection_id = self.selection[object_id]
|
||||
self.delete(selection_id)
|
||||
|
||||
# delete node and related edges
|
||||
if object_id in self.nodes:
|
||||
canvas_node = self.nodes.pop(object_id)
|
||||
canvas_node.delete()
|
||||
nodes.append(canvas_node)
|
||||
is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type)
|
||||
|
||||
# delete related edges
|
||||
for edge in canvas_node.edges:
|
||||
if edge in edges:
|
||||
continue
|
||||
edges.add(edge)
|
||||
self.throughput_draw.delete(edge)
|
||||
del self.edges[edge.token]
|
||||
edge.delete()
|
||||
|
||||
# update node connected to edge being deleted
|
||||
other_id = edge.src
|
||||
other_interface = edge.src_interface
|
||||
if edge.src == object_id:
|
||||
other_id = edge.dst
|
||||
other_interface = edge.dst_interface
|
||||
other_node = self.nodes[other_id]
|
||||
other_node.edges.remove(edge)
|
||||
try:
|
||||
other_node.interfaces.remove(other_interface)
|
||||
except ValueError:
|
||||
pass
|
||||
if is_wireless:
|
||||
other_node.delete_antenna()
|
||||
|
||||
# delete shape
|
||||
if object_id in self.shapes:
|
||||
shape = self.shapes.pop(object_id)
|
||||
shape.delete()
|
||||
|
||||
self.selection.clear()
|
||||
return nodes
|
||||
|
||||
def zoom(self, event, factor=None):
|
||||
if not factor:
|
||||
factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
|
||||
event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
|
||||
self.scale(tk.ALL, event.x, event.y, factor, factor)
|
||||
self.configure(scrollregion=self.bbox(tk.ALL))
|
||||
self.ratio *= float(factor)
|
||||
self.offset = (
|
||||
self.offset[0] * factor + event.x * (1 - factor),
|
||||
self.offset[1] * factor + event.y * (1 - factor),
|
||||
)
|
||||
logging.info("ratio: %s", self.ratio)
|
||||
logging.info("offset: %s", self.offset)
|
||||
self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%")
|
||||
|
||||
if self.wallpaper:
|
||||
self.redraw_wallpaper()
|
||||
|
||||
def click_press(self, event):
|
||||
"""
|
||||
Start drawing an edge if mouse click is on a node
|
||||
|
||||
:param event: mouse event
|
||||
:return: nothing
|
||||
"""
|
||||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
return
|
||||
|
||||
self.cursor = x, y
|
||||
selected = self.get_selected(event)
|
||||
logging.debug("click press(%s): %s", self.cursor, selected)
|
||||
x_check = self.cursor[0] - self.offset[0]
|
||||
y_check = self.cursor[1] - self.offset[1]
|
||||
logging.debug("clock press ofset(%s, %s)", x_check, y_check)
|
||||
is_node = selected in self.nodes
|
||||
if self.mode == GraphMode.EDGE and is_node:
|
||||
x, y = self.coords(selected)
|
||||
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
|
||||
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
if is_marker(self.annotation_type):
|
||||
r = self.app.toolbar.marker_tool.radius
|
||||
self.create_oval(
|
||||
x - r,
|
||||
y - r,
|
||||
x + r,
|
||||
y + r,
|
||||
fill=self.app.toolbar.marker_tool.color,
|
||||
outline="",
|
||||
tags="marker",
|
||||
)
|
||||
return
|
||||
if selected is None:
|
||||
shape = Shape(self.app, self, self.annotation_type, x, y)
|
||||
self.selected = shape.id
|
||||
self.shape_drawing = True
|
||||
self.shapes[shape.id] = shape
|
||||
|
||||
if selected is not None:
|
||||
if selected not in self.selection:
|
||||
if selected in self.shapes:
|
||||
shape = self.shapes[selected]
|
||||
self.select_object(shape.id)
|
||||
self.selected = selected
|
||||
elif selected in self.nodes:
|
||||
node = self.nodes[selected]
|
||||
self.select_object(node.id)
|
||||
self.selected = selected
|
||||
logging.info(
|
||||
"selected coords: (%s, %s)",
|
||||
node.core_node.position.x,
|
||||
node.core_node.position.y,
|
||||
)
|
||||
else:
|
||||
logging.debug("create selection box")
|
||||
if self.mode == GraphMode.SELECT:
|
||||
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
|
||||
self.select_box = shape
|
||||
self.clear_selection()
|
||||
|
||||
def ctrl_click(self, event):
|
||||
# update cursor location
|
||||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
return
|
||||
|
||||
self.cursor = x, y
|
||||
|
||||
# handle multiple selections
|
||||
logging.debug("control left click: %s", event)
|
||||
selected = self.get_selected(event)
|
||||
if (
|
||||
selected not in self.selection
|
||||
and selected in self.shapes
|
||||
or selected in self.nodes
|
||||
):
|
||||
self.select_object(selected, choose_multiple=True)
|
||||
|
||||
def click_motion(self, event):
|
||||
"""
|
||||
Redraw drawing edge according to the current position of the mouse
|
||||
|
||||
:param event: mouse event
|
||||
:return: nothing
|
||||
"""
|
||||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
if self.select_box:
|
||||
self.select_box.delete()
|
||||
self.select_box = None
|
||||
if is_draw_shape(self.annotation_type) and self.shape_drawing:
|
||||
shape = self.shapes.pop(self.selected)
|
||||
shape.delete()
|
||||
self.shape_drawing = False
|
||||
return
|
||||
|
||||
x_offset = x - self.cursor[0]
|
||||
y_offset = y - self.cursor[1]
|
||||
self.cursor = x, y
|
||||
|
||||
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
|
||||
x1, y1, _, _ = self.coords(self.drawing_edge.id)
|
||||
self.coords(self.drawing_edge.id, x1, y1, x, y)
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
if is_draw_shape(self.annotation_type) and self.shape_drawing:
|
||||
shape = self.shapes[self.selected]
|
||||
shape.shape_motion(x, y)
|
||||
elif is_marker(self.annotation_type):
|
||||
r = self.app.toolbar.marker_tool.radius
|
||||
self.create_oval(
|
||||
x - r,
|
||||
y - r,
|
||||
x + r,
|
||||
y + r,
|
||||
fill=self.app.toolbar.marker_tool.color,
|
||||
outline="",
|
||||
tags="marker",
|
||||
)
|
||||
return
|
||||
|
||||
if self.mode == GraphMode.EDGE:
|
||||
return
|
||||
|
||||
# move selected objects
|
||||
if self.selection:
|
||||
for selected_id in self.selection:
|
||||
if selected_id in self.shapes:
|
||||
shape = self.shapes[selected_id]
|
||||
shape.motion(x_offset, y_offset)
|
||||
|
||||
if selected_id in self.nodes:
|
||||
node = self.nodes[selected_id]
|
||||
node.motion(x_offset, y_offset, update=self.core.is_runtime())
|
||||
else:
|
||||
if self.select_box and self.mode == GraphMode.SELECT:
|
||||
self.select_box.shape_motion(x, y)
|
||||
|
||||
def click_context(self, event):
|
||||
logging.info("context event: %s", self.context)
|
||||
if not self.context:
|
||||
selected = self.get_selected(event)
|
||||
canvas_node = self.nodes.get(selected)
|
||||
if canvas_node:
|
||||
logging.debug(f"node context: {selected}")
|
||||
self.context = canvas_node.create_context()
|
||||
self.context.post(event.x_root, event.y_root)
|
||||
else:
|
||||
self.hide_context()
|
||||
|
||||
def press_delete(self, event):
|
||||
"""
|
||||
delete selected nodes and any data that relates to it
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
logging.debug("press delete key")
|
||||
nodes = self.delete_selection_objects()
|
||||
self.core.delete_graph_nodes(nodes)
|
||||
|
||||
def double_click(self, event):
|
||||
selected = self.get_selected(event)
|
||||
if selected is not None and selected in self.shapes:
|
||||
shape = self.shapes[selected]
|
||||
dialog = ShapeDialog(self.app, self.app, shape)
|
||||
dialog.show()
|
||||
|
||||
def add_node(self, x, y):
|
||||
if self.selected is None or self.selected in self.shapes:
|
||||
actual_x, actual_y = self.get_actual_coords(x, y)
|
||||
core_node = self.core.create_node(
|
||||
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
||||
)
|
||||
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
self.nodes[node.id] = node
|
||||
return node
|
||||
|
||||
def width_and_height(self):
|
||||
"""
|
||||
retrieve canvas width and height in pixels
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
x0, y0, x1, y1 = self.coords(self.grid)
|
||||
canvas_w = abs(x0 - x1)
|
||||
canvas_h = abs(y0 - y1)
|
||||
return canvas_w, canvas_h
|
||||
|
||||
def get_wallpaper_image(self):
|
||||
width = int(self.wallpaper.width * self.ratio)
|
||||
height = int(self.wallpaper.height * self.ratio)
|
||||
image = self.wallpaper.resize((width, height), Image.ANTIALIAS)
|
||||
return image
|
||||
|
||||
def draw_wallpaper(self, image, x=None, y=None):
|
||||
if x is None and y is None:
|
||||
x1, y1, x2, y2 = self.bbox(self.grid)
|
||||
x = (x1 + x2) / 2
|
||||
y = (y1 + y2) / 2
|
||||
|
||||
self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER)
|
||||
self.wallpaper_drawn = image
|
||||
|
||||
def wallpaper_upper_left(self):
|
||||
self.delete(self.wallpaper_id)
|
||||
|
||||
# create new scaled image, cropped if needed
|
||||
width, height = self.width_and_height()
|
||||
image = self.get_wallpaper_image()
|
||||
cropx = image.width
|
||||
cropy = image.height
|
||||
if image.width > width:
|
||||
cropx = image.width
|
||||
if image.height > height:
|
||||
cropy = image.height
|
||||
cropped = image.crop((0, 0, cropx, cropy))
|
||||
image = ImageTk.PhotoImage(cropped)
|
||||
|
||||
# draw on canvas
|
||||
x1, y1, _, _ = self.bbox(self.grid)
|
||||
x = (cropx / 2) + x1
|
||||
y = (cropy / 2) + y1
|
||||
self.draw_wallpaper(image, x, y)
|
||||
|
||||
def wallpaper_center(self):
|
||||
"""
|
||||
place the image at the center of canvas
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.delete(self.wallpaper_id)
|
||||
|
||||
# dimension of the cropped image
|
||||
width, height = self.width_and_height()
|
||||
image = self.get_wallpaper_image()
|
||||
cropx = 0
|
||||
if image.width > width:
|
||||
cropx = (image.width - width) / 2
|
||||
cropy = 0
|
||||
if image.height > height:
|
||||
cropy = (image.height - height) / 2
|
||||
x1 = 0 + cropx
|
||||
y1 = 0 + cropy
|
||||
x2 = image.width - cropx
|
||||
y2 = image.height - cropy
|
||||
cropped = image.crop((x1, y1, x2, y2))
|
||||
image = ImageTk.PhotoImage(cropped)
|
||||
self.draw_wallpaper(image)
|
||||
|
||||
def wallpaper_scaled(self):
|
||||
"""
|
||||
scale image based on canvas dimension
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.delete(self.wallpaper_id)
|
||||
canvas_w, canvas_h = self.width_and_height()
|
||||
image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS)
|
||||
image = ImageTk.PhotoImage(image)
|
||||
self.draw_wallpaper(image)
|
||||
|
||||
def resize_to_wallpaper(self):
|
||||
self.delete(self.wallpaper_id)
|
||||
image = ImageTk.PhotoImage(self.wallpaper)
|
||||
self.redraw_canvas((image.width(), image.height()))
|
||||
self.draw_wallpaper(image)
|
||||
|
||||
def redraw_canvas(self, dimensions=None):
|
||||
logging.info("redrawing canvas to dimensions: %s", dimensions)
|
||||
|
||||
# reset scale and move back to original position
|
||||
logging.info("resetting scaling: %s %s", self.ratio, self.offset)
|
||||
factor = 1 / self.ratio
|
||||
self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
|
||||
self.move(tk.ALL, -self.offset[0], -self.offset[1])
|
||||
|
||||
# reset ratio and offset
|
||||
self.ratio = 1.0
|
||||
self.offset = (0, 0)
|
||||
|
||||
# redraw canvas rectangle
|
||||
self.draw_canvas(dimensions)
|
||||
|
||||
# redraw gridlines to new canvas size
|
||||
self.delete(tags.GRIDLINE)
|
||||
self.draw_grid()
|
||||
self.update_grid()
|
||||
|
||||
def redraw_wallpaper(self):
|
||||
if self.adjust_to_dim.get():
|
||||
logging.info("drawing wallpaper to canvas dimensions")
|
||||
self.resize_to_wallpaper()
|
||||
else:
|
||||
option = ScaleOption(self.scale_option.get())
|
||||
logging.info("drawing canvas using scaling option: %s", option)
|
||||
if option == ScaleOption.UPPER_LEFT:
|
||||
self.wallpaper_upper_left()
|
||||
elif option == ScaleOption.CENTERED:
|
||||
self.wallpaper_center()
|
||||
elif option == ScaleOption.SCALED:
|
||||
self.wallpaper_scaled()
|
||||
elif option == ScaleOption.TILED:
|
||||
logging.warning("tiled background not implemented yet")
|
||||
|
||||
# raise items above wallpaper
|
||||
for component in tags.ABOVE_WALLPAPER_TAGS:
|
||||
self.tag_raise(component)
|
||||
|
||||
def update_grid(self):
|
||||
logging.info("updating grid show: %s", self.show_grid.get())
|
||||
if self.show_grid.get():
|
||||
self.itemconfig(tags.GRIDLINE, state=tk.NORMAL)
|
||||
else:
|
||||
self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN)
|
||||
|
||||
def set_wallpaper(self, filename):
|
||||
logging.info("setting wallpaper: %s", filename)
|
||||
if filename:
|
||||
img = Image.open(filename)
|
||||
self.wallpaper = img
|
||||
self.wallpaper_file = filename
|
||||
self.redraw_wallpaper()
|
||||
else:
|
||||
if self.wallpaper_id is not None:
|
||||
self.delete(self.wallpaper_id)
|
||||
self.wallpaper = None
|
||||
self.wallpaper_file = None
|
||||
|
||||
def is_selection_mode(self):
|
||||
return self.mode == GraphMode.SELECT
|
||||
|
||||
def create_edge(self, source, dest):
|
||||
"""
|
||||
create an edge between source node and destination node
|
||||
|
||||
:param CanvasNode source: source node
|
||||
:param CanvasNode dest: destination node
|
||||
:return: nothing
|
||||
"""
|
||||
if (source.id, dest.id) not in self.edges:
|
||||
pos0 = source.core_node.position
|
||||
x0 = pos0.x
|
||||
y0 = pos0.y
|
||||
edge = CanvasEdge(x0, y0, x0, y0, source.id, self)
|
||||
edge.complete(dest.id)
|
||||
self.edges[edge.token] = edge
|
||||
self.nodes[source.id].edges.add(edge)
|
||||
self.nodes[dest.id].edges.add(edge)
|
||||
self.core.create_link(edge, source, dest)
|