├── tests ├── __init__.py ├── utils │ └── __init__.py ├── mock │ └── extensions │ │ └── unit_test_mock └── run_tests.py ├── debian ├── compat ├── source │ └── format ├── rules ├── install ├── links ├── changelog ├── commotion-linux-py.postinst └── control ├── commotion_client ├── __init__.py ├── GUI │ ├── __init__.py │ ├── ui │ │ ├── __init__.py │ │ ├── welcome_page.ui │ │ └── crash_report_window.ui │ ├── welcome_page.py │ ├── system_tray.py │ ├── extension_toolbar.py │ ├── toolbar_builder.py │ ├── toolbar.py │ ├── menu_bar.py │ ├── crash_report.py │ └── main_window.py ├── assets │ ├── __init__.py │ ├── images │ │ ├── load32.png │ │ ├── logo16.png │ │ ├── logo32.png │ │ ├── logo48.png │ │ ├── logo62.png │ │ ├── save32.png │ │ ├── user32.png │ │ ├── alert32.png │ │ ├── alert48.png │ │ ├── alert62.png │ │ ├── loading62.gif │ │ ├── logo1024.png │ │ ├── logo256.png │ │ ├── logo512.png │ │ ├── settings32.png │ │ ├── full_screen_end32.png │ │ ├── full_screen_start32.png │ │ ├── question_mark_filled20.png │ │ └── question_mark_filled41.png │ ├── README │ ├── stylesheets │ │ └── forms.ss │ └── commotion_assets.qrc ├── tests │ ├── __init__.py │ ├── extensions │ │ ├── __init__.py │ │ └── test_ext001 │ │ │ ├── mainWin.ui │ │ │ ├── myMain.py │ │ │ └── warning001.ui │ └── configs │ │ └── extension │ │ └── test_ext001.conf ├── utils │ ├── __init__.py │ ├── thread.py │ ├── settings.py │ ├── single_application.py │ ├── fs_utils.py │ ├── logger.py │ └── validate.py ├── extensions │ ├── __init__.py │ ├── config_editor │ │ ├── __init__.py │ │ ├── ui │ │ │ └── __init__.py │ │ ├── test.py │ │ ├── config_editor.conf │ │ └── main.py │ └── unit_test_mock │ │ ├── units.py │ │ ├── __init__.py │ │ ├── ui │ │ └── __init__.py │ │ ├── test.py │ │ ├── test.conf │ │ ├── test_bar.py │ │ └── main.py ├── commotion_client.pyw └── commotion_client.py ├── docs ├── extensions │ ├── tutorial │ │ └── images │ │ │ └── design │ │ │ └── ssid_sketch.png │ ├── extension_template │ │ ├── config.json │ │ ├── task_bar.py │ │ ├── settings.py │ │ ├── test_suite.py │ │ └── main.py │ └── writing_extensions.md └── style_standards │ ├── README.md │ └── google_docstring_example.py ├── fallback.py ├── .gitignore ├── Makefile ├── LICENSE.txt ├── setup.py ├── README.md └── commotionc.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/GUI/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/GUI/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/assets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /commotion_client/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/tests/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/extensions/config_editor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/units.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/extensions/config_editor/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | %: 3 | dh $@ --with python2 4 | 5 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | commotionc.py usr/share/pyshared/ 2 | fallback.py usr/share/pyshared/ 3 | -------------------------------------------------------------------------------- /commotion_client/extensions/config_editor/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | def hello(): 5 | a = 5 6 | -------------------------------------------------------------------------------- /tests/mock/extensions/unit_test_mock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/tests/mock/extensions/unit_test_mock -------------------------------------------------------------------------------- /commotion_client/assets/images/load32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/load32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo16.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo48.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo62.png -------------------------------------------------------------------------------- /commotion_client/assets/images/save32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/save32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/user32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/user32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/alert32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/alert32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/alert48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/alert48.png -------------------------------------------------------------------------------- /commotion_client/assets/images/alert62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/alert62.png -------------------------------------------------------------------------------- /commotion_client/assets/images/loading62.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/loading62.gif -------------------------------------------------------------------------------- /commotion_client/assets/images/logo1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo1024.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo256.png -------------------------------------------------------------------------------- /commotion_client/assets/images/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/logo512.png -------------------------------------------------------------------------------- /commotion_client/assets/images/settings32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/settings32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/full_screen_end32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/full_screen_end32.png -------------------------------------------------------------------------------- /commotion_client/assets/images/full_screen_start32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/full_screen_start32.png -------------------------------------------------------------------------------- /docs/extensions/tutorial/images/design/ssid_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/docs/extensions/tutorial/images/design/ssid_sketch.png -------------------------------------------------------------------------------- /debian/links: -------------------------------------------------------------------------------- 1 | usr/share/pyshared/commotionc.py usr/lib/python2.6/dist-packages/commotionc.py 2 | usr/share/pyshared/commotionc.py usr/lib/python2.7/dist-packages/commotionc.py 3 | -------------------------------------------------------------------------------- /commotion_client/assets/images/question_mark_filled20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/question_mark_filled20.png -------------------------------------------------------------------------------- /commotion_client/assets/images/question_mark_filled41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentechinstitute/commotion-client/HEAD/commotion_client/assets/images/question_mark_filled41.png -------------------------------------------------------------------------------- /commotion_client/extensions/config_editor/config_editor.conf: -------------------------------------------------------------------------------- 1 | { 2 | "name":"config_editor", 3 | "menu_item":"Commotion Config File Editor", 4 | "parent":"Advanced", 5 | "main":"main" 6 | } 7 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | commotion-linux-py (0.2-1) unstable; urgency=low 2 | 3 | * Initial release. (Closes: #XXXXXX) 4 | 5 | -- Jordan Wed, 28 Aug 2013 11:59:50 -0400 6 | -------------------------------------------------------------------------------- /commotion_client/commotion_client.pyw: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | """ 5 | The Windows entry point for the Commotion_client. 6 | """ 7 | 8 | from commotion_client import main 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /commotion_client/tests/configs/extension/test_ext001.conf: -------------------------------------------------------------------------------- 1 | { 2 | "name":"test_ext001", 3 | "menuItem":"Test Extension 001", 4 | "parent":"Test Suite", 5 | "settings":"mySettings", 6 | "taskbar":"myTaskBar", 7 | "main":"myMain" 8 | } -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/test.conf: -------------------------------------------------------------------------------- 1 | { 2 | "name":"unit_test_mock", 3 | "menu_item":"A Mock Testing Object", 4 | "parent":"Testing", 5 | "main":"main", 6 | "settings":"main", 7 | "toolbar":"test_bar", 8 | "tests":"units" 9 | } 10 | -------------------------------------------------------------------------------- /docs/extensions/extension_template/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"extension_template", 3 | "menuItem":"Extension Template", 4 | "parent":"Templates", 5 | "settings":"settings", 6 | "taskbar":"task_bar", 7 | "main":"main", 8 | "tests":"test_suite" 9 | } 10 | -------------------------------------------------------------------------------- /debian/commotion-linux-py.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip install pyjavaproperties 3 | IBSS_RSN="`/sbin/wpa_cli ibss_rsn 00:00:00:00:00`" 4 | if [[ "$IBSS_RSN" == *UNKNOWN* ]]; then 5 | echo "\nWARNING: The version of wpasupplicant installed on your system does not support encrypted mesh connections. If you intend to participate in encrypted mesh networks, you will need to install commotion-wpasupplicant, or manually recompile a newer version of wpasupplicant (1.0+) with the IBSS_RSN=y flag set. \nPress any key to continue..." 6 | read -n 1 -s 7 | fi 8 | -------------------------------------------------------------------------------- /commotion_client/assets/README: -------------------------------------------------------------------------------- 1 | When a new asset is added to the Commotion_client project the assets.py file MUST be re-created. The file assets.py within this folder is created by doing the following. 2 | 3 | When a new asset is created it must be added to the commotion_assets.qrc xml file. If that asset requires translation make sure to add translated versions to the appropriate language resource sections. e.g if the lanaguge == french. . 4 | 5 | For testing the commotion_client without using the build scripts you must create the assets file by running the following command line argument. 6 | 7 | 8 | pyrcc4 -py3 commotion_assets.qrc -o commotion_assets_rc.py 9 | 10 | 11 | When new assets are added or old assets are changed... really any change to any assets you MUST run this command. This will re-create the assets.py file. -------------------------------------------------------------------------------- /docs/extensions/extension_template/task_bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | task_bar 6 | 7 | The task bar for the extension template. 8 | 9 | @brief The object that contains the custom task-bar. If not set and a "taskbar" class is not found in the file listed under the "main" option the default taskbar will be implemented. 10 | 11 | @note This template ONLY includes the objects for the "task bar" component of the extension template. The other components can be found in their respective locations. 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | 18 | #PyQt imports 19 | from PyQt4 import QtCore 20 | from PyQt4 import QtGui 21 | 22 | #import python modules created by qtDesigner and converted using pyuic4 23 | from GUI import task_bar 24 | 25 | 26 | class TaskBar(task_bar.TaskBar): 27 | """ 28 | 29 | """ 30 | 31 | -------------------------------------------------------------------------------- /fallback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import commotionc 4 | import subprocess 5 | import sys 6 | 7 | profile = sys.argv[1] 8 | operation = sys.argv[2] 9 | 10 | commotion = commotionc.CommotionCore('commotion-fallback') 11 | 12 | if operation == 'up': 13 | commotion.fallbackConnect(sys.argv[1]) 14 | 15 | if operation == 'down': 16 | commotion.log('Bringing down mesh connection. The six following lines should show return values of zero') 17 | commotion.log(str(subprocess.call(['pkill', '-9', 'commotion_wpa']))) 18 | commotion.log(str(subprocess.call(['pkill', '-9', 'olsrd']))) 19 | commotion.log(str(subprocess.call(['nmcli', 'nm', 'sleep', 'false']))) 20 | commotion.log(str(subprocess.call(['rfkill', 'block', 'wifi']))) 21 | commotion.log(str(subprocess.call(['rfkill', 'unblock', 'wifi']))) 22 | 23 | # Add full up/down/status logic (if up, then; if down, then) 24 | # Add ArgumentParser logic 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Debian .deb cruft 2 | debian/files 3 | debian/commotion-linux-py.debhelper.log 4 | debian/commotion-linux-py.substvars 5 | debian/commotion-linux-py.postinst.debhelper 6 | debian/commotion-linux-py.prerm.debhelper 7 | debian/commotion-linux-py/ 8 | 9 | # python build products 10 | *.pyc 11 | __pycache__ 12 | 13 | # example code 14 | examples/ 15 | 16 | # PyQt auto-created asset tracking file. 17 | *_rc.py 18 | commotion_client/temp/ 19 | 20 | #All compiled versions of designer created UI files. 21 | Ui_*.py 22 | 23 | # Emacs auto-save cruft because s2e does not want to spend the time debugging his .emacs config right now. 24 | \#.*# 25 | 26 | #Extension Application Data 27 | commotion_client/data/extensions/* 28 | 29 | #compiled clients 30 | build/exe* 31 | build/lib 32 | build/resources 33 | 34 | #testing objects 35 | tests/temp/* 36 | 37 | #auto-created commotion on failure of everywhere else 38 | commotion.log 39 | -------------------------------------------------------------------------------- /commotion_client/tests/extensions/test_ext001/mainWin.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | testExt001 4 | 5 | 6 | 7 | 0 8 | 0 9 | 758 10 | 564 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 280 23 | 220 24 | 201 25 | 121 26 | 27 | 28 | 29 | Press Me 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /commotion_client/utils/thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | thread 6 | 7 | Implementation of a generic thread class. 8 | 9 | Key componenets handled within: 10 | 11 | """ 12 | import logging 13 | 14 | from PyQt4 import QtCore 15 | import time 16 | 17 | class GenericThread(QtCore.QThread): 18 | def __init__(self): 19 | QtCore.QThread.__init__(self) 20 | self.log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. 21 | self.log.debug(QtCore.QCoreApplication.translate("logs","Created thread.")) 22 | 23 | def __del__(self): 24 | self.wait() 25 | 26 | def run(self): 27 | self.log.debug(QtCore.QCoreApplication.translate("logs","Starting thread.")) 28 | return 29 | 30 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build windows osx debian clean install tests 2 | 3 | all: build 4 | 5 | build: clean assets 6 | python3.3 build/scripts/build.py build 7 | python3.3 build/scripts/zip_extensions.py 8 | 9 | assets: 10 | mkdir build/resources || true 11 | pyrcc4 -py3 commotion_client/assets/commotion_assets.qrc -o build/resources/commotion_assets_rc.py 12 | 13 | windows: 14 | @echo "windows compileing is not yet implemented" 15 | 16 | osx: 17 | @echo "macintosh saddening is not yet implemented" 18 | 19 | linux: build 20 | python3.3 setup.py build 21 | 22 | debian: 23 | @echo "debian packaging is not yet implemented" 24 | 25 | test: tests 26 | @echo "test build complete" 27 | 28 | tests: build 29 | mkdir tests/temp || true 30 | mkdir tests/mock/assets || true 31 | cp build/resources/commotion_assets_rc.py tests/mock/assets/. || true 32 | python3.3 tests/run_tests.py 33 | 34 | clean: 35 | python3.3 build/scripts/build.py clean 36 | rm -fr build/resources || true 37 | rm -fr build/exe.* || true 38 | rm -fr tests/temp/* || true 39 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: commotion-linux-py 2 | Maintainer: Jordan McCarthy 3 | Homepage: https://github.opentechinstitute.org/commotion-linux-py 4 | Section: net 5 | Priority: extra 6 | Build-Depends: python-all (>= 2.6.6-3~) 7 | 8 | Package: commotion-linux-py 9 | Architecture: all 10 | Depends: ${shlibs:Depends}, 11 | ${misc:Depends}, 12 | python, 13 | python-support, 14 | olsrd, 15 | olsrd-plugins, 16 | python-pyjavaproperties, 17 | python-pip 18 | Recommends: nm-dispatcher-olsrd, 19 | commotion-mesh-applet, 20 | commotion-wpasupplicant 21 | Description: Implements core Commotion (mesh networking with olsrd, serval, etc.) functionality as a python module 22 | Provides fundamental Commotion routines for use by a variety of front-end utilities. Also provides a custom-compiled 23 | version of wpa_supplicant for use on older systems whose stock wpa_supplicant binaries were compiled without support 24 | for encrypted mesh connections (IBSS-RSN). 25 | -------------------------------------------------------------------------------- /docs/extensions/extension_template/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | settings 6 | 7 | The settings page for the extension template. 8 | 9 | @brief The settings page for the extension. This page controls how the extension level settings should look and behave in the settings menu. If this is not included in the config file and a "settings" class is not found in the file listed under the "main" option the extension will not list a settings button in the extension settings page. 10 | 11 | @note This template ONLY includes the objects for the "settings" component of the extension template. The other components can be found in their respective locations. 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | 18 | #PyQt imports 19 | from PyQt4 import QtCore 20 | from PyQt4 import QtGui 21 | 22 | #import python modules created by qtDesigner and converted using pyuic4 23 | from extensions.extension_template.ui import Ui_settings 24 | 25 | class Settings(Ui_settings.ViewPort): 26 | """ 27 | """ 28 | 29 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Evaluation tests for Commotion Networks. 3 | 4 | """ 5 | import unittest 6 | import importlib 7 | import time 8 | import sys 9 | import os 10 | import faulthandler 11 | 12 | def create_runner(verbosity_level=None): 13 | """creates a testing runner. 14 | 15 | suite_type: (string) suites to run [acceptable values = suite_types in build_suite()] 16 | """ 17 | faulthandler.enable() 18 | loader = unittest.TestLoader() 19 | tests = loader.discover('.', '*_tests.py') 20 | testRunner = unittest.runner.TextTestRunner(verbosity=verbosity_level, warnings="always") 21 | testRunner.run(tests) 22 | 23 | 24 | if __name__ == '__main__': 25 | """Creates argument parser for required arguments and calls test runner""" 26 | import argparse 27 | parser = argparse.ArgumentParser(description='openThreads test suite') 28 | parser.add_argument("-v", "--verbosity", nargs="?", default=2, const=2, dest="verbosity_level", metavar="VERBOSITY", help="make test_suite verbose") 29 | 30 | args = parser.parse_args() 31 | create_runner(args.verbosity_level) 32 | 33 | -------------------------------------------------------------------------------- /commotion_client/GUI/welcome_page.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 6 | welcome_page 7 | 8 | The welcome page for the main window. 9 | 10 | Key components handled within. 11 | * being pretty and welcoming to new users 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | 18 | #PyQt imports 19 | from PyQt4 import QtCore 20 | from PyQt4 import QtGui 21 | 22 | from commotion_client.GUI.ui import Ui_welcome_page 23 | 24 | class ViewPort(Ui_welcome_page.ViewPort): 25 | """ 26 | """ 27 | start_report_collection = QtCore.pyqtSignal() 28 | data_report = QtCore.pyqtSignal(str, dict) 29 | error_report = QtCore.pyqtSignal(str) 30 | on_stop = QtCore.pyqtSignal() 31 | 32 | 33 | def __init__(self, parent=None): 34 | super().__init__() 35 | self.log = logging.getLogger("commotion_client."+__name__) 36 | self.setupUi(self) #run setup function from Ui_main_window 37 | self._dirty = False 38 | 39 | @property 40 | def is_dirty(self): 41 | """The current state of the viewport object """ 42 | return self._dirty 43 | 44 | def clean_up(self): 45 | self.on_stop.emit() 46 | 47 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/test_bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_bar 6 | 7 | A unit test extension. Not for production. 8 | """ 9 | 10 | #Standard Library Imports 11 | import logging 12 | import sys 13 | #PyQt imports 14 | from PyQt4 import QtCore 15 | from PyQt4 import QtGui 16 | 17 | #import python modules created by qtDesigner and converted using pyuic4 18 | from ui import Ui_test 19 | 20 | class ToolBar(Ui_test.ViewPort): 21 | """ 22 | This is a mock extension and should not be used for ANYTHING user facing! 23 | """ 24 | 25 | start_report_collection = QtCore.pyqtSignal() 26 | data_report = QtCore.pyqtSignal(str, dict) 27 | error_report = QtCore.pyqtSignal(str) 28 | 29 | def __init__(self, parent=None): 30 | super().__init__() 31 | self.setupUi(self) 32 | self.start_report_collection.connect(self.send_signal) 33 | 34 | def send_signal(self): 35 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 36 | 37 | def send_error(self): 38 | """HI""" 39 | self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") 40 | pass 41 | 42 | def is_loaded(self): 43 | return True 44 | -------------------------------------------------------------------------------- /commotion_client/tests/extensions/test_ext001/myMain.py: -------------------------------------------------------------------------------- 1 | #Standard Library Imports 2 | import logging 3 | 4 | #PyQt imports 5 | from PyQt4 import QtCore 6 | from PyQt4 import QtGui 7 | 8 | #import python modules created by qtDesigner and converted using pyuic4 9 | from tests.extensions.test_ext001 import Ui_warning001 10 | from tests.extensions.test_ext001 import Ui_mainWin 11 | 12 | 13 | class viewport(QtGui.QWidget, Ui_mainWin.Ui_testExt001): #inheret from both widget and out object 14 | 15 | #signals for crash reporter 16 | start_report_collection = QtCore.pyqtSignal() 17 | data_report = QtCore.pyqtSignal(str, dict) 18 | error_report = QtCore.pyqtSignal(str) 19 | 20 | def __init__(self, parent=None): 21 | super().__init__() 22 | self.setupUi(self) #run setup function from pyuic4 object 23 | self.start_report_collection.connect(self.send_signal) 24 | #make central app main button turn on crash reporter 25 | self.warningButton.clicked.connect(self.send_error) 26 | 27 | 28 | def send_signal(self): 29 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 30 | 31 | def send_error(self): 32 | self.error_report.emit("THIS IS AN ERROR") 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyleft (c) 2013, NAF/OTI Commotion Wireless Project 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /commotion_client/assets/stylesheets/forms.ss: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Commotion style sheet for entry forms. 4 | 5 | Color Pallet: 6 | 7 | PRIMARY COLORS 8 | * White FFFFFF 9 | * Black 000000 10 | * Pink FF739C 11 | 12 | SECONDARY COLORS 13 | * Electric Yellow E8FF00 14 | * Electric Purple 877AED 15 | * Electric Green 00FFcF 16 | * Blue 63CCF5 17 | * Gold C7BA38 18 | * Grey E6E6E6 19 | 20 | Color Usage Ratio: 21 | * 70% White 22 | * 15% Black 23 | * 10% Pink 24 | * 5% Electric Purple 25 | 26 | Font Sizeing: 27 | * 40 px Headings 28 | * 13 Px [ALL CAPS]: Subheadings 29 | * 13 px: Body Text 30 | per: https://github.com/opentechinstitute/commotion-docs/blob/staging/commotionwireless.net/files/HIG_57_0.png *They meant pixel, not point. 31 | 32 | */ 33 | 34 | /* Defaults */ 35 | * { font-size: 13px; } 36 | 37 | /* Section Header */ 38 | .QLabel[style_sheet_type = "section_header"] { 39 | font-size: 40px; 40 | font-style: bold; 41 | } 42 | 43 | /* Value Header */ 44 | .QLabel[style_sheet_type = "value_header"] { 45 | color: #877AED; 46 | font-style: bold; 47 | } 48 | 49 | /* Help Pop Up */ 50 | .QToolTip[style_sheet_type = "value_help_text"] { background-color: #E6E6E6 } 51 | 52 | /* Help Text */ 53 | .QLabel[style_sheet_type = "help_text"] { font-style: italic; } 54 | 55 | /* Static / Automatic / Unchangable Value */ 56 | .QLabel[style_sheet_type = "help_text"] { background-color: #E6E6E6 } 57 | -------------------------------------------------------------------------------- /docs/extensions/extension_template/test_suite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_suite 6 | 7 | The test suite for the extension template. 8 | 9 | """ 10 | 11 | #Standard Library Imports 12 | import sys 13 | import unittest 14 | 15 | #PyQt imports 16 | from PyQt4 import QtCore 17 | from PyQt4 import QtGui 18 | from PyQt4.QtTest import QtTest 19 | 20 | #import python modules created by qtDesigner and converted using pyuic4 21 | from extensions.extension_template import main 22 | from extensions.extension_template import settings 23 | from extensions.extension_template import task_bar 24 | 25 | class MainTest(unittest.TestCase): 26 | """ 27 | Test the main viewport object. 28 | """ 29 | 30 | def setUp(self): 31 | self.app = QtGui.QApplication(sys.argv) 32 | self.task_bar = main.ViewPort() 33 | 34 | class SettingsTest(unittest.TestCase): 35 | """ 36 | Test the settings object. 37 | """ 38 | 39 | def setUp(self): 40 | self.app = QtGui.QApplication(sys.argv) 41 | self.task_bar = settings.ViewPort() 42 | 43 | class TaskBarTest(unittest.TestCase): 44 | """ 45 | Test the task bar object 46 | """ 47 | 48 | def setUp(self): 49 | self.app = QtGui.QApplication(sys.argv) 50 | self.task_bar = task_bar.TaskBar() 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /commotion_client/GUI/system_tray.py: -------------------------------------------------------------------------------- 1 | #Standard Library Imports 2 | import logging 3 | 4 | #PyQt imports 5 | from PyQt4 import QtCore 6 | from PyQt4 import QtGui 7 | 8 | #Commotion Client Imports 9 | import commotion_assets_rc 10 | 11 | class TrayIcon(QtGui.QWidget): 12 | """ 13 | The Commotion tray icon. This icon object is the only object that can close the entire application. 14 | """ 15 | show_main = QtCore.pyqtSignal() 16 | 17 | def __init__(self, parent=None): 18 | super().__init__() 19 | self.log = logging.getLogger("commotion_client."+__name__) #TODO stop hard_coding commotion_ client 20 | #Create actions for tray menu 21 | self.exit = QtGui.QAction(QtGui.QIcon(), "Exit", self) 22 | #set tray Icon and it's menu which allows closing from it. 23 | self.tray_icon = QtGui.QSystemTrayIcon(QtGui.QIcon(":logo32.png"), self) 24 | menu = QtGui.QMenu(self) 25 | menu.addAction(self.exit) #add exit action to tray icon 26 | self.tray_icon.setContextMenu(menu) 27 | self.tray_icon.activated.connect(self.tray_iconActivated) 28 | self.tray_icon.show() 29 | 30 | def tray_iconActivated(self, reason): 31 | """ 32 | Defines the tray icon behavior on different types of interactions. 33 | """ 34 | if reason == QtGui.QSystemTrayIcon.Context: 35 | self.tray_icon.contextMenu().show() 36 | elif reason == QtGui.QSystemTrayIcon.Trigger: 37 | self.show_main.emit() 38 | -------------------------------------------------------------------------------- /commotion_client/GUI/ui/welcome_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ViewPort 4 | 5 | 6 | 7 | 0 8 | 0 9 | 640 10 | 480 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 240 20 | 200 21 | 144 22 | 77 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | :/logo62.png 33 | 34 | 35 | Qt::AlignCenter 36 | 37 | 38 | 39 | 40 | 41 | 42 | Commotion Computer 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /commotion_client/assets/commotion_assets.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | images/alert32.png 6 | images/alert48.png 7 | images/alert62.png 8 | 9 | images/save32.png 10 | images/load32.png 11 | images/user32.png 12 | images/settings32.png 13 | images/full_screen_start32.png 14 | images/full_screen_end32.png 15 | 16 | images/question_mark_filled41.png 17 | images/question_mark_filled20.png 18 | 19 | images/loading62.gif 20 | 21 | images/logo16.png 22 | images/logo32.png 23 | images/logo48.png 24 | images/logo62.png 25 | images/logo256.png 26 | images/logo512.png 27 | images/logo1024.png 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/extensions/extension_template/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | main 6 | 7 | An initial viewport template to make development easier. 8 | 9 | @brief Populates the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. 10 | 11 | @note This template ONLY includes the objects for the "main" component of the extension template. The other components can be found in their respective locations. 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | 18 | #PyQt imports 19 | from PyQt4 import QtCore 20 | from PyQt4 import QtGui 21 | 22 | #import python modules created by qtDesigner and converted using pyuic4 23 | from extensions.contrib.extension_template.ui import Ui_main 24 | 25 | class ViewPort(Ui_main.ViewPort): 26 | """ 27 | 28 | """ 29 | #Signals for data collection, reporting, and alerting on errors 30 | start_report_collection = QtCore.pyqtSignal() 31 | data_report = QtCore.pyqtSignal(str, dict) 32 | error_report = QtCore.pyqtSignal(str) 33 | on_stop = QtCore.pyqtSignal() 34 | 35 | def __init__(self, parent=None): 36 | super().__init__() 37 | self._dirty = False 38 | self.setupUi(self) 39 | self.start_report_collection.connect(self.send_signal) 40 | 41 | def send_signal(self): 42 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 43 | 44 | def send_error(self): 45 | self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") 46 | 47 | @property 48 | def is_dirty(self): 49 | """The current state of the viewport object """ 50 | return self.dirty 51 | 52 | def clean_up(self): 53 | self.on_stop.emit() 54 | -------------------------------------------------------------------------------- /commotion_client/extensions/unit_test_mock/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | main 6 | 7 | A unit test extension. Not for production. 8 | 9 | """ 10 | 11 | #Standard Library Imports 12 | import logging 13 | import sys 14 | #PyQt imports 15 | from PyQt4 import QtCore 16 | from PyQt4 import QtGui 17 | 18 | #import python modules created by qtDesigner and converted using pyuic4 19 | from ui import Ui_test 20 | 21 | class ViewPort(Ui_test.ViewPort): 22 | """ 23 | This is a mock extension and should not be used for ANYTHING user facing! 24 | """ 25 | 26 | start_report_collection = QtCore.pyqtSignal() 27 | data_report = QtCore.pyqtSignal(str, dict) 28 | error_report = QtCore.pyqtSignal(str) 29 | 30 | def __init__(self, parent=None): 31 | super().__init__() 32 | self.setupUi(self) 33 | self.start_report_collection.connect(self.send_signal) 34 | 35 | def send_signal(self): 36 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 37 | 38 | def send_error(self): 39 | """HI""" 40 | self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") 41 | pass 42 | 43 | def is_loaded(self): 44 | return True 45 | 46 | 47 | class SettingsMenu(Ui_test.ViewPort): 48 | """ 49 | This is a mock extension and should not be used for ANYTHING user facing! 50 | """ 51 | 52 | start_report_collection = QtCore.pyqtSignal() 53 | data_report = QtCore.pyqtSignal(str, dict) 54 | error_report = QtCore.pyqtSignal(str) 55 | 56 | def __init__(self, parent=None): 57 | super().__init__() 58 | self.setupUi(self) 59 | self.start_report_collection.connect(self.send_signal) 60 | 61 | def send_signal(self): 62 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 63 | 64 | def send_error(self): 65 | """HI""" 66 | self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") 67 | pass 68 | 69 | def is_loaded(self): 70 | return True 71 | -------------------------------------------------------------------------------- /commotion_client/utils/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 5 | This program is a part of The Commotion Client 6 | 7 | Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | """ 23 | 24 | """ 25 | CURRENTLY A DEVELOPMENT STUB! 26 | 27 | 28 | setting.py 29 | 30 | The Settings Manager 31 | 32 | Key componenets handled within: 33 | * Loading and Unloading User Settings Files 34 | * Validating the scope of settings 35 | 36 | """ 37 | #Standard Library Imports 38 | import logging 39 | 40 | #PyQt imports 41 | from PyQt4 import QtCore 42 | 43 | #Commotion Client Imports 44 | 45 | 46 | class UserSettingsManager(object): 47 | 48 | def __init__(self): 49 | """Create a settings object that is tied to a specific scope. 50 | CURRENTLY A DEVELOPMENT STUB! 51 | """ 52 | self.settings = QtCore.QSettings() 53 | 54 | def save(self): 55 | """CURRENTLY A DEVELOPMENT STUB!""" 56 | #call PGP to save temporary file to correct encrypted file 57 | pass 58 | 59 | def load(self): 60 | """CURRENTLY A DEVELOPMENT STUB!""" 61 | 62 | #call pgp to get location of decrypted user file, if any 63 | #load global settings file. 64 | # QSettings.setUserIniPath (QString dir) 65 | #get 66 | pass 67 | 68 | def get(self): 69 | """CURRENTLY A DEVELOPMENT STUB!""" 70 | return self.settings 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /commotion_client/extensions/config_editor/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | main 6 | 7 | An initial viewport template to make development easier. 8 | 9 | @brief Populates the extensions initial view-port. This can be the same file as the settings and taskbar as long as that file contains seperate functions for each object type. 10 | 11 | @note This template ONLY includes the objects for the "main" component of the extension template. The other components can be found in their respective locations. 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | import sys 18 | #PyQt imports 19 | from PyQt4 import QtCore 20 | from PyQt4 import QtGui 21 | 22 | 23 | from commotion_client.GUI import extension_toolbar 24 | 25 | #import python modules created by qtDesigner and converted using pyuic4 26 | #from extensions.core.config_manager.ui import Ui_config_manager.py 27 | from ui import Ui_config_manager 28 | 29 | class ViewPort(Ui_config_manager.ViewPort): 30 | """ 31 | pineapple 32 | """ 33 | 34 | start_report_collection = QtCore.pyqtSignal() 35 | data_report = QtCore.pyqtSignal(str, dict) 36 | error_report = QtCore.pyqtSignal(str) 37 | clean_up = QtCore.pyqtSignal() 38 | on_stop = QtCore.pyqtSignal() 39 | 40 | def __init__(self, parent=None): 41 | super().__init__() 42 | self.log = logging.getLogger("commotion_client."+__name__) 43 | self.translate = QtCore.QCoreApplication.translate 44 | self.setupUi(self) 45 | self.start_report_collection.connect(self.send_signal) 46 | self._dirty = False 47 | 48 | @property 49 | def is_dirty(self): 50 | """The current state of the viewport object """ 51 | return self._dirty 52 | 53 | def clean_up(self): 54 | self.on_stop.emit() 55 | 56 | def send_signal(self): 57 | self.data_report.emit("myModule", {"value01":"value", "value02":"value", "value03":"value"}) 58 | 59 | def send_error(self): 60 | """HI""" 61 | self.error_report.emit("THIS IS AN ERROR MESSAGE!!!") 62 | pass 63 | 64 | 65 | 66 | class ToolBar(extension_toolbar.ExtensionToolBar): 67 | pass 68 | -------------------------------------------------------------------------------- /commotion_client/tests/extensions/test_ext001/warning001.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 368 10 | 196 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 20 | 150 21 | 341 22 | 32 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 30 | 31 | 32 | 33 | 34 | 35 | 50 36 | 50 37 | 291 38 | 131 39 | 40 | 41 | 42 | You clicked a button. 43 | 44 | 45 | Qt::AlignCenter 46 | 47 | 48 | 49 | 50 | 51 | 52 | buttonBox 53 | accepted() 54 | Dialog 55 | accept() 56 | 57 | 58 | 248 59 | 254 60 | 61 | 62 | 157 63 | 274 64 | 65 | 66 | 67 | 68 | buttonBox 69 | rejected() 70 | Dialog 71 | reject() 72 | 73 | 74 | 316 75 | 260 76 | 77 | 78 | 286 79 | 274 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 5 | This program is a part of The Commotion Client 6 | 7 | Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | """ 23 | """ 24 | setup.py 25 | 26 | This module includes the cx_freeze functionality for building the bundled extensions. 27 | 28 | You can find further documentation on this in the build/ directory under README.md. 29 | """ 30 | import os 31 | import sys 32 | #import the setup.py version of setup 33 | from cx_Freeze import setup, Executable 34 | 35 | #---------- OS Setup -----------# 36 | 37 | # GUI applications require a different base on Windows (the default is for a 38 | # console application). 39 | base = None 40 | if sys.platform == "win32": 41 | base = "Win32GUI" 42 | 43 | # Windows requires the icon to be specified in the setup.py. 44 | icon = "commotion_client/assets/images/logo32.png" 45 | 46 | #---------- Packages -----------# 47 | 48 | # Define core packages. 49 | core_pkgs = ["commotion_client", "utils", "GUI", "assets"] 50 | 51 | # Include compiled assets file. 52 | assets_file = os.path.join("build", "resources", "commotion_assets_rc.py") 53 | # Place compiled assets file into the root directory. 54 | include_assets = (assets_file, "commotion_assets_rc.py") 55 | all_assets = [include_assets] 56 | 57 | 58 | #======== ADD EXTENSIONS HERE ==============# 59 | 60 | # Define bundled "core" extensions here. 61 | core_extensions = ["config_editor"] 62 | 63 | #===========================================# 64 | 65 | # Add core_extensions to core packages. 66 | for ext in core_extensions: 67 | ext_loc = os.path.join("build", "resources", ext) 68 | asset_loc = os.path.join("extensions", "core", ext) 69 | all_assets.append((ext_loc, asset_loc)) 70 | 71 | 72 | #---------- Executable Setup -----------# 73 | 74 | exe = Executable( 75 | targetName="Commotion", 76 | script="commotion_client/commotion_client.py", 77 | packages=core_pkgs, 78 | ) 79 | 80 | #---------- Core Setup -----------# 81 | 82 | setup(name="Commotion Client", 83 | version="1.0", 84 | url="commotionwireless.net", 85 | license="Affero General Public License V3 (AGPLv3)", 86 | executables = [exe], 87 | options = {"build_exe":{"include_files": all_assets}} 88 | ) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![alt tag](http://img.shields.io/badge/maintainer-technosopher-lightgray.svg)](https://github.com/technosopher) 2 | ##Commotion Client (UNSTABLE) 3 | 4 | The Commotion Wireless desktop/laptop client. 5 | 6 | To allow desktop clients to create, connect to, and configure Commotion wireless mesh networks. 7 | 8 | This repository is in active development. **IT DOES NOT WORK!** Please look at the roadmap below to see where the project is currently at. 9 | 10 | ###FUTURE Features: 11 | 12 | * A graphical user interface with: 13 | * A "setup wizard" for quickly creating/connecting to a Commotion mesh. 14 | * Mesh network advances settings configuration tools 15 | * Commotion mesh config customizer 16 | * Application system with: 17 | * Mesh network application viewer 18 | * Client application advertisement 19 | * Multiple user accounts with: 20 | * Seperate "Serval Keychains" 21 | * Custom Network & Application Settings 22 | * A status bar icon for selecting, connecting to, and disconnecting from ad-hoc networks 23 | * A robust extension system that allows for easy customization and extension of the core platform 24 | * Full string translation & internationalization support 25 | * Built in accessability support 26 | 27 | ###Requirements: ( To run ) 28 | 29 | * Python 3 or higher 30 | 31 | ###Requirements: ( To build from source ) 32 | 33 | * Python 3.3 or higher 34 | * cx_freeze (See: build/README.md for instructions) 35 | 36 | ###Current Roadmap: 37 | 38 | #### Version 1.0 39 | 40 | * Core application 41 | * Single application support 42 | * Cross-application instance messaging 43 | * Crash reporting 44 | * With PGP encryption to the Commotion Team 45 | * Crash Reporting Window 46 | * Main Window 47 | * Menu Bar 48 | * Automatically displays all core and user loaded extensions 49 | * Task Bar 50 | * Extension Manager 51 | * Messaging manager 52 | * Allow extensions to talk to commotion IPC client 53 | * CSM and Commotiond support 54 | * Core Extensions 55 | * Commotion Config File Editor 56 | * Setup Wizard (basic config walkthough) 57 | * Application Viewer 58 | * Application Advertiser 59 | * Welcome Page 60 | * Network Security Menu 61 | * Network Status overview 62 | * Setting menu 63 | * Core application settings 64 | * Extension settings menu 65 | * Settings for any extensions with custom settings pages 66 | * Control Panel settings menu 67 | * A client agnostic control panel tool for mesh-network settings in an operating systems generic control panel. 68 | * Linux Support 69 | * Commotion Human Interface Guidelines compliant interface 70 | * In-Line Documentation tranlation into developer API 71 | * User Settings Manager 72 | * un-encrypted user settings for network configuration 73 | 74 | #### Version 2.0 75 | 76 | * Setting menu 77 | * User settings 78 | * Core Extensions 79 | * Network vizualizer 80 | * User Settings [applications] 81 | * User Settings [Serval & Security] 82 | * REMOVE Network Security Menu as it will be replaced with user settings 83 | * User Settings Manager 84 | * GPG Encrypted user settings 85 | * multi-user login/logout support 86 | 87 | ### Version 3+.0 88 | * Windows Support 89 | * OSX Support 90 | -------------------------------------------------------------------------------- /docs/style_standards/README.md: -------------------------------------------------------------------------------- 1 | # Code Standards 2 | 3 | ## Style 4 | 5 | The code base should comply with [PEP 8](http://legacy.python.org/dev/peps/pep-0008/) styling. 6 | 7 | ## Documentation and Doc-Strings 8 | 9 | Doc Strings should follow the Google style docstrings shown in the google_docstring_example.py file contained in this folder. 10 | 11 | ## Logging 12 | 13 | ### Code 14 | 15 | #### Proper Logging 16 | 17 | Every functional file should import the "logging" standard library and create a logger that is a decendant of the main commotion_client logger. 18 | 19 | ``` 20 | import logging 21 | 22 | ... 23 | 24 | self.log = logging.getLogger("commotion_client."+__name__) 25 | ``` 26 | 27 | #### Logging and translation 28 | 29 | We use the PyQt translate library to translate text in the Commotion client. The string ``logs``` is used as the "context" for all logging objects. While the translate library will automatically add the class name as the context for most translated strings we would like to seperate out logging strings so that translators working with the project can prioritize it less than critical user facing text. 30 | 31 | ``` 32 | _error = QtCore.QCoreApplication.translate("logs", "That is not a valid extension config value.") 33 | self.log.error(_error) 34 | ``` 35 | 36 | Due to the long length of the translation call ``QtCore.QCoreApplication.translate``` feel free to set this value to the variable self.translate at the start of any classes. Please refrain from using another variable name to maintain consistancy actoss the code base. 37 | 38 | ```self.translate = QtCore.QCoreApplication.translate``` 39 | 40 | ### LogLevels 41 | 42 | Logging should correspond to the following levels: 43 | 44 | * critical: The application is going to need to close. There is no possible recovery or alternative behavior. This will generate an error-report (if possible) and is ABSOLUTELY a bug that will need to be addressed if a user reports seeing one of these logs. 45 | 46 | * error & exception: The application is in distress and has visibly failed to do what was requested of it by the user. These do not have to close the application, and may have failsafes or handling, but should be severe enough to be reported to the user. If a user experiences one of these the application has failed in a way that is a programmers fault. These can generate an error-report at the programmers discression. 47 | 48 | * warn: An unexpected event has occured. A user may be affected, but adaquate fallbacks and handling can still provide the user with a smooth experience. These are the issues that need to be tracked, but are not neccesarily a bug, but simply the application handling inconsistant environmental conditions or usage. 49 | 50 | * info: Things you want to see at high volume in case you need to forensically analyze an issue. System lifecycle events (system start, stop) go here. "Session" lifecycle events (login, logout, etc.) go here. Significant boundary events should be considered as well (e.g. database calls, remote API calls). Typical business exceptions can go here (e.g. login failed due to bad credentials). Any other event you think you'll need to see in production at high volume goes here. 51 | 52 | * debug: Just about everything that doesn't make the "info" cut... any message that is helpful in tracking the flow through the system and isolating issues, especially during the development and QA phases. We use "debug" level logs for entry/exit of most non-trivial methods and marking interesting events and decision points inside methods. 53 | 54 | ### Logging Exeptions 55 | 56 | Exceptions should be logged using the exception handle at the point where they interfeir with the core task. When an exception is handled in the program it should be logged on the debug level. A short description of why an exception is raised should be logged at the debug level wherever an excetion is first raised in the program. 57 | 58 | tldr: If you raise an exception, log what happened, but let whomever handles it log the traceback as debug. 59 | 60 | ## Exception Handling 61 | 62 | 63 | ## Code 64 | 65 | ### Python Version 66 | All code MUST be compatable with Python3. 67 | -------------------------------------------------------------------------------- /commotion_client/utils/single_application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | single_application 6 | 7 | The single application handler classes the commotion client inherets its cross process communication from. 8 | 9 | Key componenets handled within: 10 | * singleApplication mode 11 | * cross instance messaging 12 | 13 | """ 14 | 15 | import logging 16 | 17 | from PyQt4 import QtGui 18 | from PyQt4 import QtCore 19 | from PyQt4 import QtNetwork 20 | 21 | 22 | class SingleApplication(QtGui.QApplication): 23 | """ 24 | Single application instance uses a key and shared memory to ensure that only one instance of the Commotion client is ever running at the same time. 25 | """ 26 | 27 | def __init__(self, key, argv): 28 | super().__init__(argv) 29 | 30 | #set function logger 31 | self.log = logging.getLogger("commotion_client."+__name__) 32 | 33 | #Keep Track of main widgets, so as not to recreate them. 34 | self.main = False 35 | self.status_bar = False 36 | self.control_panel = False 37 | #Check for shared memory from other instances and if not created, create them. 38 | self._key = key 39 | self.shared_memory = QtCore.QSharedMemory(self) 40 | self.shared_memory.setKey(key) 41 | if self.shared_memory.attach(): 42 | self._is_running = True 43 | else: 44 | self._is_running = False 45 | if not self.shared_memory.create(1): 46 | self.log.info(self.translate("logs", "Application shared memory already exists.")) 47 | raise RuntimeError(self.shared_memory.errorString()) 48 | 49 | def is_running(self): 50 | return self._is_running 51 | 52 | 53 | class SingleApplicationWithMessaging(SingleApplication): 54 | """ 55 | The interprocess messaging class for the Commotion Client. This class extends the single application to allow for instantiations of the Commotion Client to pass messages to the existing client if it is already running. When a second instance of a Commotion Client is run without a message specified it will reaise the earler clients main window to the front and then close itself. 56 | 57 | e.g: 58 | python3.3 CommotionClient.py --message "COMMAND" 59 | """ 60 | 61 | def __init__(self, key, argv): 62 | super().__init__(key, argv) 63 | 64 | self._key = key 65 | self._timeout = 1000 66 | #create server to listen for messages 67 | self._server = QtNetwork.QLocalServer(self) 68 | #Connect to messageAvailable signal created by handle_message. 69 | self.connect(self, QtCore.SIGNAL('messageAvailable'), self.process_message) 70 | 71 | if not self.is_running(): 72 | bytes.decode 73 | self._server.newConnection.connect(self.handle_message) 74 | self._server.listen(self._key) 75 | 76 | def handle_message(self): 77 | """ 78 | Server side implementation of the messaging functions. This function waits for signals it receives and then emits a SIGNAL "messageAvailable" with the decoded message. 79 | 80 | (Emits a signal instead of just calling a function in case we decide we would like to allow other components or extensions to listen for messages from new instances.) 81 | """ 82 | socket = self._server.nextPendingConnection() 83 | if socket.waitForReadyRead(self._timeout): 84 | self.emit(QtCore.SIGNAL("messageAvailable"), bytes(socket.readAll().data()).decode('utf-8')) 85 | socket.disconnectFromServer() 86 | self.log.debug(self.translate("logs", "message received and emitted in a messageAvailable signal")) 87 | else: 88 | self.log.error(socket.errorString()) 89 | 90 | def send_message(self, message): 91 | """ 92 | Message sending function. Connected to local socket specified by shared key and if successful writes the message to it and returns. 93 | """ 94 | if self.is_running(): 95 | socket = QtNetwork.QLocalSocket(self) 96 | socket.connectToServer(self._key, QtCore.QIODevice.WriteOnly) 97 | if not socket.waitForConnected(self._timeout): 98 | self.log.error(socket.errorString()) 99 | return False 100 | socket.write(str(message).encode("utf-8")) 101 | if not socket.waitForBytesWritten(self._timeout): 102 | self.log.error(socket.errorString()) 103 | return False 104 | socket.disconnectFromServer() 105 | return True 106 | self.log.debug(self.translate("logs", "Attempted to send message when commotion client application was not currently running.")) 107 | return False 108 | 109 | def process_message(self, message): 110 | """ 111 | Process which processes messages an app receives and takes actions on valid requests. 112 | """ 113 | self.log.debug(self.translate("logs", "Applicaiton received a message {0}, but does not have a message parser to handle it.").format(message)) 114 | -------------------------------------------------------------------------------- /commotion_client/GUI/extension_toolbar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Extension Toolbar 6 | 7 | The toolbar object extensions can use to derive extra menu items from. 8 | 9 | """ 10 | #Standard Library Imports 11 | import logging 12 | 13 | #PyQt imports 14 | from PyQt4 import QtCore 15 | from PyQt4 import QtGui 16 | 17 | #Commotion Client Imports 18 | import commotion_assets_rc 19 | 20 | class ExtensionToolBar(object): 21 | """ 22 | The central widget for the commotion client. This widget initalizes all other sub-widgets and extensions as well as defines the paramiters of the main GUI container. 23 | 24 | 25 | An example of adding a single button to the menu that calls a function "save_form()" 26 | 27 | new_button = MenuItem 28 | my_button.setIcon(icon.save) 29 | my_button.setText(self.translate("menu", "Save")) 30 | new_button.action = self.save_form 31 | self.add_item(new_button) 32 | 33 | 34 | """ 35 | 36 | def __init__(self, viewport): 37 | """Sets up all the translation, logging, and core items needed for an extension toolbar. 38 | 39 | Args: 40 | extension_menu_items (object): The extension specific menu-item to be used by an extension. This class is derived from the 41 | viewport (object): The extensions viewport. This allows menu_items to have its actions interact with the current viewport. 42 | """ 43 | super().__init__() 44 | self._dirty = False 45 | self.log = logging.getLogger("commotion_client."+__name__) 46 | self.translate = QtCore.QCoreApplication.translate 47 | self.viewport = viewport 48 | self.menu_items = {} 49 | #The basic set of icons for extensions 50 | self.icon = { 51 | "save":QtGui.QIcon(":save32.png"), 52 | "load":QtGui.QIcon(":load32.png"), 53 | "user":QtGui.QIcon(":user32.png"), 54 | "settings":QtGui.QIcon(":settings32.png"), 55 | "full_screen_start":QtGui.QIcon(":full_screen_start32.png"), 56 | "full_screen_end":QtGui.QIcon(":full_screen_end32.png"), 57 | } 58 | 59 | def add_item(self, tool_button): 60 | if tool_button.icon().isNull(): 61 | tool_button.setIcon(self.icon.settings) 62 | self.menu_items.append(tool_button) 63 | 64 | class MenuItem(QtGui.QToolButton): 65 | """The menu_item template object 66 | 67 | To Make a basic toolbar button simply run the following. 68 | 69 | #From within the ExtensionToolBar 70 | my_button = MenuItem 71 | my_button.setIcon(self.icon.save) 72 | my_button.setText(self.translate("menu", "Save")) 73 | my_button.triggered.connect(self.my_save_function) 74 | 75 | To Make a toolbar with a menu run the following. 76 | 77 | #From within the ExtensionToolBar 78 | my_menu = MenuItem 79 | my_menu.setIcon(self.icon.settings) 80 | my_menu.setText(self.translate("menu", "Options")) 81 | my_menu.set_menu(True) 82 | menu_save = QtGui.QAction("Save", icons.save, self.my_save_function) 83 | my_menu.sub_menu.addAction(menu_save) 84 | #Using a custom icon from an extension. 85 | menu_load = QtGui.QAction("Load", QtGui.QIcon("icons/load.png"), statusTip=self.translate("menu", "Load a item from a file"), triggered=self.my_load_function) 86 | my_menu.menu.addAction(menu_load) 87 | 88 | menuItems are QToolButtons 89 | menuItems that have sub menu's are composed of a QMenu with QActions within it. 90 | 91 | QToolButton:http://pyqt.sourceforge.net/Docs/PyQt4/qtoolbutton.html 92 | QMenu: http://pyqt.sourceforge.net/Docs/PyQt4/qmenu.html 93 | QAction: http://pyqt.sourceforge.net/Docs/PyQt4/qaction.html 94 | 95 | """ 96 | 97 | def __init__(self, parent=None, viewport=None): 98 | """Sets up all the core components needed for a minimal menuItem 99 | 100 | Args: 101 | viewport (object): The current viewport. This allows menu_items to have its actions interact with the current viewport. 102 | 103 | """ 104 | super().__init__() 105 | self._dirty = False 106 | self.log = logging.getLogger("commotion_client."+__name__) 107 | self.translate = QtCore.QCoreApplication.translate 108 | self.viewport = viewport 109 | 110 | def set_menu(self, value): 111 | if value == True: 112 | self.log.debug(self.translate("logs", "Setting toolbar item {0} to be a menu.".format(self.text()))) 113 | #Set menu to pop up immediately. 114 | self.setPopupMode(QtGui.QToolButton.InstantPopup) 115 | #Create a new menu and set it 116 | self.sub_menu = QtGui.QMenu(self) 117 | self.setMenu(self.sub_menu) 118 | elif value == False: 119 | self.log.debug(self.translate("logs", "Setting toolbar item {0} to NOT be a menu.".format(self.text()))) 120 | #Remove the menu if it exists 121 | self.sub_menu = None 122 | else: 123 | self.log.debug(self.translate("logs", "{0} is not a proper value for set_menu. Please use a bool value True or False.".format(value))) 124 | raise ValueError(self.translate("logs", "Attempted to set the menu state to an invalid value.")) 125 | 126 | -------------------------------------------------------------------------------- /commotion_client/utils/fs_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | fs_utils 6 | 7 | 8 | """ 9 | 10 | #PyQt imports 11 | from PyQt4 import QtCore 12 | 13 | #Standard Library Imports 14 | import os 15 | import logging 16 | import uuid 17 | import json 18 | 19 | translate = QtCore.QCoreApplication.translate 20 | log = logging.getLogger("commotion_client."+__name__) 21 | 22 | 23 | def is_file(unknown): 24 | """Determines if a file is accessable. It does NOT check to see if the file contains any data. 25 | 26 | Args: 27 | unknown (string): The path to check for a accessable file. 28 | 29 | Returns: 30 | bool True if a file is accessable and readable, False if a file is unreadable, or unaccessable. 31 | 32 | """ 33 | translate = QtCore.QCoreApplication.translate 34 | this_file = QtCore.QFile(str(unknown)) 35 | if not this_file.exists(): 36 | log.warn(translate("logs","The file {0} does not exist.".format(str(unknown)))) 37 | return False 38 | if not os.access(unknown, os.R_OK): 39 | log.warn(translate("logs","You do not have permission to access the file {0}".format(str(unknown)))) 40 | return False 41 | return True 42 | 43 | def walklevel(some_dir, level=1): 44 | some_dir = some_dir.rstrip(os.path.sep) 45 | log.debug(translate("logs", "attempting to walk directory {0}".format(some_dir))) 46 | if not os.path.isdir(some_dir): 47 | raise NotADirectoryError(translate("logs", "{0} is not a directory. Can only 'walk' down through directories.".format(some_dir))) 48 | num_sep = some_dir.count(os.path.sep) 49 | for root, dirs, files in os.walk(some_dir): 50 | yield root, dirs, files 51 | num_sep_this = root.count(os.path.sep) 52 | if num_sep + level <= num_sep_this: 53 | del dirs[:] 54 | 55 | def make_temp_dir(new=None): 56 | """Makes a temporary directory and returns the QDir object. 57 | 58 | @param new bool Create a new uniquely named directory within the exiting Commotion temp directory and return the new folder object 59 | """ 60 | log = logging.getLogger("commotion_client."+__name__) 61 | temp_path = "Commotion" 62 | temp_dir = QtCore.QDir.tempPath() 63 | if new: 64 | unique_dir_name = uuid.uuid4() 65 | temp_path = os.path.join(temp_path, str(unique_dir_name)) 66 | temp_full = QtCore.QDir(os.path.join(temp_dir, temp_path)) 67 | if temp_full.mkpath(temp_full.path()): 68 | log.debug(QtCore.QCoreApplication.translate("logs", "Creating main temporary directory")) 69 | else: 70 | _error = QtCore.QCoreApplication.translate("logs", "Error creating temporary directory") 71 | log.debug(_error) 72 | raise IOError(_error) 73 | return temp_full 74 | 75 | 76 | def clean_dir(path=None): 77 | """ Cleans a directory. If not given a path it will clean the FULL temporary directory""" 78 | log = logging.getLogger("commotion_client."+__name__) 79 | if not path: 80 | path = QtCore.QDir(os.path.join(QtCore.QDir.tempPath(), "Commotion")) 81 | path.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) 82 | list_of_files = path.entryInfoList() 83 | 84 | for file_info in list_of_files: 85 | file_path = file_info.absoluteFilePath() 86 | if not QtCore.QFile(file_path).remove(): 87 | _error = QtCore.QCoreApplication.translate("logs", "Error saving extension to extensions directory.") 88 | log.error(_error) 89 | raise IOError(_error) 90 | path.rmpath(path.path()) 91 | return True 92 | 93 | def copy_contents(start, end): 94 | """ Copies the contents of one directory into another 95 | 96 | @param start QDir A Qdir object for the first directory 97 | @param end QDir A Qdir object for the final directory 98 | """ 99 | log = logging.getLogger("commotion_client."+__name__) 100 | start.setFilter(QtCore.QDir.NoSymLinks | QtCore.QDir.Files) 101 | list_of_files = start.entryInfoList() 102 | 103 | for file_info in list_of_files: 104 | source = file_info.absoluteFilePath() 105 | dest = os.path.join(end.path(), file_info.fileName()) 106 | if not QtCore.QFile(source).copy(dest): 107 | _error = QtCore.QCoreApplication.translate("logs", "Error copying file into extensions directory. File already exists.") 108 | log.error(_error) 109 | raise IOError(_error) 110 | return True 111 | 112 | def json_load(path): 113 | """This function loads a JSON file and returns a formatted dictionary. 114 | 115 | Args: 116 | path (string): The path to a json formatted file. 117 | 118 | Returns: 119 | The JSON data from the file formatted as a dictionary. 120 | 121 | Raises: 122 | TypeError: The file could not be opened due to an unknown error. 123 | ValueError: The file was of an invalid type (eg. not in utf-8 format, etc.) 124 | 125 | """ 126 | translate = QtCore.QCoreApplication.translate 127 | log = logging.getLogger("commotion_client."+__name__) 128 | 129 | #Open the file 130 | try: 131 | f = open(string, mode='r', encoding="utf-8", errors="strict") 132 | except ValueError: 133 | log.warn(translate("logs", "Config files must be in utf-8 format to avoid data loss. The config file {0} is improperly formatted ".format(path))) 134 | raise 135 | except TypeError: 136 | log.warn(translate("logs", "An unknown error has occured in opening config file {0}. Please check that this file is the correct type.".format(path))) 137 | raise 138 | else: 139 | tmpMsg = f.read() 140 | #Parse the JSON 141 | try: 142 | data = json.loads(tmpMsg) 143 | log.info(translate("logs", "Successfully loaded {0}".format(path))) 144 | return data 145 | except ValueError: 146 | log.warn(translate("logs", "Failed to load {0} due to a non-json or otherwise invalid file type".format(path))) 147 | raise 148 | -------------------------------------------------------------------------------- /commotion_client/GUI/toolbar_builder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Toolbar 6 | 7 | The core toolbar object for commotion viewports. 8 | 9 | The tool bar is an object that is created in the main viewport. This tool-bar has pre-built objects for common functions and an add-on section that will allow a developer building a extension to add functionality they need. 10 | 11 | """ 12 | #Standard Library Imports 13 | import logging 14 | 15 | #PyQt imports 16 | from PyQt4 import QtCore 17 | from PyQt4 import QtGui 18 | 19 | #Commotion Client Imports 20 | import commotion_assets_rc 21 | from commotion_client.GUI import extension_toolbar 22 | 23 | 24 | class ToolBar(QtGui.QWidget): 25 | """ 26 | The Core toolbar object that populates manditory toolbar sections. 27 | """ 28 | 29 | def __init__(self, viewport, parent=None, extension_toolbar=None): 30 | """Creates the core toolbar including any extension toolbar passed to it. 31 | 32 | Initializes the core functionality of the toolbar. If an extension_toolbar object is also passed to the toolbar it will attempt to add the extension toolbar into itself. 33 | 34 | Args: 35 | extension_toolbar (object): The extension specific menu-item to be used by an extension. This class is derived from the "commotion_client/GUI/extension_toolbar.ExtensionToolBar" object. 36 | viewport (object): The current viewport. This allows menu_items to have its actions interact with the current viewport. 37 | 38 | Raises: 39 | exception: Description. 40 | 41 | """ 42 | super().__init__() 43 | self._dirty = False 44 | self.log = logging.getLogger("commotion_client."+__name__) 45 | self.translate = QtCore.QCoreApplication.translate 46 | 47 | self.viewport = viewport 48 | #Create toolbar object 49 | self.toolbar = QtGui.QToolBar(self) 50 | self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) 51 | #Create & add settings item 52 | self.init_settings() 53 | self.toolbar.addWidget(self.settings) 54 | #Create & add user item 55 | # self.user = self.init_user() 56 | # self.toolbar.addWidget(self.user) 57 | #Create extension toolbar section if needed 58 | # if extension_toolbar: 59 | # self.extension_toolbar = extension_toolbar(self, viewport) 60 | # self.init_extension_toolbar() 61 | 62 | def init_settings(self): 63 | """short description 64 | 65 | long description 66 | 67 | Args: 68 | name (type): Description. 69 | 70 | Returns: 71 | Description. 72 | 73 | Raises: 74 | exception: Description. 75 | 76 | """ 77 | self.settings = QtGui.QToolButton(self.toolbar) 78 | # settings = extension_toolbar.MenuItem(self.toolbar, self.viewport) 79 | # self.settings.setText(self.translate("menu", "Settings")) 80 | # settings.set_menu(True) 81 | # self.settings.setIcon(QtGui.QIcon(":logo48.png")) 82 | self.settings.setPopupMode(QtGui.QToolButton.InstantPopup) 83 | self.settings.setMenu(QtGui.QMenu(self.settings)) 84 | 85 | 86 | extensions_item = QtGui.QAction(self.translate("menu", "&Extensions"), self.settings) 87 | extensions_item.setStatusTip(self.translate("menu", "Open the extensions menu.")) 88 | extensions_item.triggered.connect(self.load_extensions) 89 | self.settings.menu().addAction(extensions_item) 90 | 91 | settings_item = QtGui.QAction(QtGui.QIcon(":settings32.png"), self.translate("menu", "&Settings"), self.settings) 92 | settings_item.setStatusTip(self.translate("menu", "Open the settings menu.")) 93 | settings_item.triggered.connect(self.load_settings) 94 | self.settings.menu().addAction(settings_item) 95 | self.settings.setDefaultAction(settings_item) 96 | 97 | about_item = QtGui.QAction(self.translate("menu", "&About"), self.settings) 98 | about_item.setStatusTip(self.translate("menu", "Open the \'about us\' page")) 99 | about_item.triggered.connect(self.load_about) 100 | self.settings.menu().addAction(about_item) 101 | 102 | exit_item = QtGui.QAction(self.translate("menu", "&Exit"), self.settings) 103 | exit_item.setStatusTip(self.translate("menu", "Exit the application.")) 104 | exit_item.triggered.connect(self.exit_application) 105 | self.settings.menu().addAction(exit_item) 106 | 107 | update_item = QtGui.QAction(self.translate("menu", "&Update"), self.settings) 108 | update_item.setStatusTip(self.translate("menu", "Open the updates page.")) 109 | update_item.triggered.connect(self.load_update) 110 | self.settings.menu().addAction(update_item) 111 | 112 | 113 | def load_settings(self): 114 | """Opens the settings menu in the main viewport """ 115 | pass 116 | 117 | def load_about(self): 118 | """Opens the about page in the main viewport """ 119 | pass 120 | 121 | def load_update(self): 122 | """Opens the updates menu in the main viewport """ 123 | pass 124 | 125 | def load_user(self): 126 | """Opens the user menu in the main viewport """ 127 | pass 128 | 129 | def exit_application(self): 130 | """Exits the application.""" 131 | pass 132 | 133 | def load_extensions(self): 134 | """Opens the extensions menu in the main viewport """ 135 | pass 136 | 137 | def init_user(self): 138 | """short description 139 | 140 | long description 141 | 142 | Args: 143 | name (type): Description. 144 | 145 | Returns: 146 | Description. 147 | 148 | Raises: 149 | exception: Description. 150 | 151 | """ 152 | pass 153 | 154 | 155 | 156 | def init_extension_toolbar(self): 157 | """short description 158 | 159 | long description 160 | 161 | Args: 162 | name (type): Description. 163 | 164 | Returns: 165 | Description. 166 | 167 | Raises: 168 | exception: Description. 169 | """ 170 | for menu_item in self.extension_menu.menu_items: 171 | self.toolbar.addWidget(menu_item) 172 | -------------------------------------------------------------------------------- /commotion_client/GUI/toolbar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Toolbar 6 | 7 | The core toolbar object for commotion viewports. 8 | 9 | The tool bar is an object that is created in the main viewport. This tool-bar has pre-built objects for common functions and an add-on section that will allow a developer building a extension to add functionality they need. 10 | 11 | 12 | """ 13 | #Standard Library Imports 14 | import logging 15 | 16 | #PyQt imports 17 | from PyQt4 import QtCore 18 | from PyQt4 import QtGui 19 | 20 | #Commotion Client Imports 21 | from commotion_client.assets import commotion_assets_rc 22 | from commotion_client.GUI import extension_toolbar 23 | 24 | 25 | class ToolBar(QtGui.QWidget): 26 | """ 27 | The Core toolbar object that populates manditory toolbar sections. 28 | """ 29 | 30 | def __init__(self, parent=None, extension_toolbar=None, viewport): 31 | """Creates the core toolbar including any extension toolbar passed to it. 32 | 33 | Initializes the core functionality of the toolbar. If an extension_toolbar object is also passed to the toolbar it will attempt to add the extension toolbar into itself. 34 | 35 | Args: 36 | extension_toolbar (object): The extension specific menu-item to be used by an extension. This class is derived from the "commotion_client/GUI/extension_toolbar.ExtensionToolBar" object. 37 | viewport (object): The current viewport. This allows menu_items to have its actions interact with the current viewport. 38 | 39 | Raises: 40 | exception: Description. 41 | 42 | """ 43 | super().__init__() 44 | self._dirty = False 45 | self.log = logging.getLogger("commotion_client."+__name__) 46 | self.translate = QtCore.QCoreApplication.translate 47 | 48 | self.viewport = viewport 49 | #Create toolbar object 50 | self.toolbar = QtGui.QToolBar(self) 51 | #Create & add settings item 52 | self.settings = self.init_settings() 53 | self.toolbar.addWidget(self.settings) 54 | #Create & add user item 55 | # self.user = self.init_user() 56 | # self.toolbar.addWidget(self.user) 57 | #Create extension toolbar section if needed 58 | # if extension_toolbar: 59 | # self.extension_toolbar = extension_toolbar(self, viewport) 60 | # self.init_extension_toolbar() 61 | 62 | def init_settings(self): 63 | """short description 64 | 65 | long description 66 | 67 | Args: 68 | name (type): Description. 69 | 70 | Returns: 71 | Description. 72 | 73 | Raises: 74 | exception: Description. 75 | 76 | """ 77 | settings_menu = extension_toolbar.MenuItem(self, self.viewport) 78 | settings_menu.setIcon(extension_toolbar.icon.settings) 79 | settings_menu.setText(self.translate("menu", "Settings")) 80 | settings_menu.set_menu = True 81 | extensions_item = QtGui.QAction(self.translate("menu", "&Extensions"), 82 | statusTip=self.translate("menu", "Open the extensions menu."), 83 | triggered=self.load_extensions, 84 | parent=settings_menu) 85 | 86 | settings_item = QtGui.QAction(self.translate("menu", "&Settings"), 87 | QtGui.QIcon("icons/load.png"), 88 | statusTip=self.translate("menu", "Open the settings menu."), 89 | triggered=self.load_settings, 90 | parent=settings_menu) 91 | about_item = QtGui.QAction(self.translate("menu", "&About"), 92 | QtGui.QIcon("icons/load.png"), 93 | statusTip=self.translate("menu", "Open the \'about us\' page"), 94 | triggered=self.load_about, 95 | parent=settings_menu) 96 | exit_item = QtGui.QAction(self.translate("menu", "&Exit"), 97 | QtGui.QIcon("icons/load.png"), 98 | statusTip=self.translate("menu", "Exit the application."), 99 | triggered=self.exit_application, 100 | parent=settings_menu) 101 | update_item = QtGui.QAction(self.translate("menu", "&Update"), 102 | QtGui.QIcon("icons/load.png"), 103 | statusTip=self.translate("menu", "Open the updates page."), 104 | triggered=self.load_update, 105 | parent=settings_menu) 106 | return settings_menu 107 | 108 | def load_settings(self): 109 | """Opens the settings menu in the main viewport """ 110 | pass 111 | 112 | def load_about(self): 113 | """Opens the about page in the main viewport """ 114 | pass 115 | 116 | def load_update(self): 117 | """Opens the updates menu in the main viewport """ 118 | pass 119 | 120 | def load_user(self): 121 | """Opens the user menu in the main viewport """ 122 | pass 123 | 124 | def exit_application(self): 125 | """Exits the application.""" 126 | pass 127 | 128 | def load_extensions(self): 129 | """Opens the extensions menu in the main viewport """ 130 | pass 131 | 132 | def init_user(self): 133 | """short description 134 | 135 | long description 136 | 137 | Args: 138 | name (type): Description. 139 | 140 | Returns: 141 | Description. 142 | 143 | Raises: 144 | exception: Description. 145 | 146 | """ 147 | pass 148 | 149 | 150 | 151 | def init_extension_toolbar(self): 152 | """short description 153 | 154 | long description 155 | 156 | Args: 157 | name (type): Description. 158 | 159 | Returns: 160 | Description. 161 | 162 | Raises: 163 | exception: Description. 164 | 165 | """ 166 | for menu_item in self.extension_menu.menu_items: 167 | try: 168 | self.toolbar.addWidget(menu_item) 169 | -------------------------------------------------------------------------------- /commotion_client/GUI/menu_bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | """ 6 | MenuBar 7 | 8 | The menu bar used for hierarchical navigation of commotion extensions. 9 | 10 | Key componenets handled within: 11 | * 12 | 13 | """ 14 | 15 | #Standard Library Imports 16 | import logging 17 | from functools import partial 18 | 19 | #PyQt imports 20 | from PyQt4 import QtCore 21 | from PyQt4 import QtGui 22 | 23 | #Commotion Client Imports 24 | from commotion_client.utils.extension_manager import ExtensionManager 25 | 26 | class MenuBar(QtGui.QWidget): 27 | 28 | #create signal used to communicate with mainWindow on viewport change 29 | viewport_requested = QtCore.pyqtSignal(str) 30 | 31 | def __init__(self, parent=None): 32 | super().__init__() 33 | 34 | self.layout = QtGui.QVBoxLayout() 35 | 36 | #set function logger 37 | self.log = logging.getLogger("commotion_client."+__name__) 38 | self.translate = QtCore.QCoreApplication.translate 39 | self.ext_mgr = ExtensionManager() 40 | try: 41 | self.populate_menu() 42 | except (NameError, AttributeError) as _excpt: 43 | self.log.info(self.translate("logs", "The Menu Bar could not populate the menu")) 44 | raise 45 | self.log.debug(QtCore.QCoreApplication.translate("logs", "Menu bar has initalized successfully.")) 46 | 47 | def request_viewport(self, viewport): 48 | """ 49 | When called will emit a request for a viewport change. 50 | """ 51 | self.log.debug(QtCore.QCoreApplication.translate("logs", "Request to change viewport sent")) 52 | self.viewport_requested.emit(viewport) 53 | 54 | def clear_layout(self, layout): 55 | """Clears a layout of all widgets. 56 | 57 | Args: 58 | layout (QLayout): A QLayout object that needs to be cleared of all objects. 59 | """ 60 | if not layout.isEmpty(): 61 | while layout.count(): 62 | item = layout.takeAt(0) 63 | widget = item.widget() 64 | if widget is not None: 65 | widget.deleteLater() 66 | else: 67 | self.clear_layout(item.layout()) 68 | 69 | def populate_menu(self): 70 | """Resets and populates the menu using loaded extensions.""" 71 | if not self.layout.isEmpty(): 72 | self.clear_layout(self.layout) 73 | menu_items = {} 74 | if not self.ext_mgr.check_installed(): 75 | self.ext_mgr.init_extension_libraries() 76 | extensions = self.ext_mgr.get_installed().keys() 77 | if extensions: 78 | top_level = self.get_parents(extensions) 79 | for top_level_item in top_level: 80 | try: 81 | current_item = self.add_menu_item(top_level_item) 82 | except NameError as _excpt: 83 | self.log.debug(self.translate("logs", "No extensions found under the parent item {0}. Parent item will not be added to the menu.".format(top_level_item))) 84 | self.log.exception(_excpt) 85 | else: 86 | if current_item: 87 | menu_items[top_level_item] = current_item 88 | if menu_items: 89 | for title, section in menu_items.items(): 90 | #Add top level menu item 91 | self.layout.addWidget(section[0]) 92 | #Add sub-menu layout 93 | self.layout.addWidget(section[1]) 94 | else: 95 | raise AttributeError(QtCore.QCoreApplication.translate("exception", "No menu items could be created from the extensions found. Please re-run the commotion client with full verbosity to identify what went wrong.")) 96 | else: 97 | raise NameError(QtCore.QCoreApplication.translate("exception", "No extensions found. Please re-run the commotion_client with full verbosity to find out what went wrong.")) 98 | self.setLayout(self.layout) 99 | 100 | def get_parents(self, extension_list): 101 | """Gets all unique parents from a list of extensions. 102 | 103 | This function gets the "parent" menu items from a list of extensions and returns a list of the unique members. 104 | 105 | Args: 106 | extension_list (list): A list containing a set of strings that list the names of extensions. 107 | 108 | Returns: 109 | A list of all the unique parents of the given extensions. 110 | 111 | ['parent item 01', 'parent item 02'] 112 | """ 113 | parents = [] 114 | for ext in extension_list: 115 | try: 116 | parent = self.ext_mgr.get_property(ext, "parent") 117 | except KeyError: 118 | self.log.debug(self.translate("logs", "Config for {0} does not contain a {1} value. Setting {1} to default value.".format(ext, "parent"))) 119 | parent = "Extensions" 120 | if parent not in parents: 121 | parents.append(parent) 122 | return parents 123 | 124 | 125 | def add_menu_item(self, parent): 126 | """Creates and returns a single top level menu item with cascading sub-menu items. 127 | 128 | Args: 129 | parent (string): The "parent" the top level menu item that is being requested. 130 | 131 | Returns: 132 | A tuple containing a top level button and its hidden sub-menu items. 133 | """ 134 | extensions = self.ext_mgr.get_extension_from_property('parent', parent) 135 | if not extensions: 136 | raise NameError(self.translate("logs", "No extensions found under the parent item {0}.".format(parent))) 137 | #Create Top level item button 138 | title_button = QtGui.QPushButton(QtCore.QCoreApplication.translate("Menu Item", parent)) 139 | title_button.setCheckable(True) 140 | #Create sub-menu 141 | sub_menu = QtGui.QFrame() 142 | sub_menu_layout = QtGui.QVBoxLayout() 143 | #populate the sub-menu item table. 144 | for ext in extensions: 145 | sub_menu_item = subMenuWidget(self) 146 | try: 147 | menu_item_title = self.ext_mgr.get_property(ext, 'menu_item') 148 | except KeyError: 149 | menu_item_title = ext 150 | sub_menu_item.setText(QtCore.QCoreApplication.translate("Sub-Menu Item", menu_item_title)) 151 | #We use partial here to pass a variable along when we attach the "clicked()" signal to the MenuBars requestViewport function 152 | sub_menu_item.clicked.connect(partial(self.request_viewport, ext)) 153 | sub_menu_layout.addWidget(sub_menu_item) 154 | sub_menu.setLayout(sub_menu_layout) 155 | sub_menu.hide() 156 | #Connect toggle on out checkable title button to the visability of our subMenu 157 | title_button.toggled.connect(sub_menu.setVisible) 158 | #package and return top level item and its corresponding subMenu 159 | section = (title_button, sub_menu) 160 | return section 161 | 162 | 163 | class subMenuWidget(QtGui.QLabel): 164 | """ 165 | This class extends QLabel to make clickable labels. 166 | """ 167 | 168 | #FUN-FACT: Signals must be created outside of the init statement because "the library distinguishes between unbound and bound signals. The magic is performed as follows: "A signal (specifically an unbound signal) is an attribute of a class that is a sub-class of QObject. When a signal is referenced as an attribute of an instance of the class then PyQt5 automatically binds the instance to the signal in order to create a bound signal." 169 | clicked = QtCore.pyqtSignal() 170 | 171 | def __init__(self, parent=None): 172 | super().__init__() 173 | 174 | def mouseReleaseEvent(self, ev): 175 | self.clicked.emit() 176 | -------------------------------------------------------------------------------- /docs/style_standards/google_docstring_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example Google style docstrings. 3 | 4 | This module demonstrates documentation as specified by the `Google Python 5 | Style Guide`_. Docstrings may extend over multiple lines. Sections are created 6 | with a section header and a colon followed by a block of indented text. 7 | 8 | Example: 9 | Examples can be given using either the ``Example`` or ``Examples`` 10 | sections. Sections support any reStructuredText formatting, including 11 | literal blocks:: 12 | 13 | $ python example_google.py 14 | 15 | Section breaks are created by simply resuming unindented text. Section breaks 16 | are also implicitly created anytime a new section starts. 17 | 18 | Attributes: 19 | module_level_variable (int): Module level variables may be documented in 20 | either the ``Attributes`` section of the module docstring, or in an 21 | inline docstring immediately following the variable. 22 | 23 | Either form is acceptable, but the two should not be mixed. Choose 24 | one convention to document module level variables and be consistent 25 | with it. 26 | 27 | .. _Google Python Style Guide: 28 | http://google-styleguide.googlecode.com/svn/trunk/pyguide.html 29 | 30 | """ 31 | 32 | module_level_variable = 12345 33 | 34 | 35 | def module_level_function(param1, param2=None, *args, **kwargs): 36 | """This is an example of a module level function. 37 | 38 | Function parameters should be documented in the ``Args`` section. The name 39 | of each parameter is required. The type and description of each parameter 40 | is optional, but should be included if not obvious. 41 | 42 | If the parameter itself is optional, it should be noted by adding 43 | ", optional" to the type. If \*args or \*\*kwargs are accepted, they 44 | should be listed as \*args and \*\*kwargs. 45 | 46 | The format for a parameter is:: 47 | 48 | name (type): description 49 | The description may span multiple lines. Following 50 | lines should be indented. 51 | 52 | Multiple paragraphs are supported in parameter 53 | descriptions. 54 | 55 | Args: 56 | param1 (int): The first parameter. 57 | param2 (str, optional): The second parameter. Defaults to None. 58 | Second line of description should be indented. 59 | *args: Variable length argument list. 60 | **kwargs: Arbitrary keyword arguments. 61 | 62 | Returns: 63 | bool: True if successful, False otherwise. 64 | 65 | The return type is optional and may be specified at the beginning of 66 | the ``Returns`` section followed by a colon. 67 | 68 | The ``Returns`` section may span multiple lines and paragraphs. 69 | Following lines should be indented to match the first line. 70 | 71 | The ``Returns`` section supports any reStructuredText formatting, 72 | including literal blocks:: 73 | 74 | { 75 | 'param1': param1, 76 | 'param2': param2 77 | } 78 | 79 | Raises: 80 | AttributeError: The ``Raises`` section is a list of all exceptions 81 | that are relevant to the interface. 82 | ValueError: If `param2` is equal to `param1`. 83 | 84 | """ 85 | if param1 == param2: 86 | raise ValueError('param1 may not be equal to param2') 87 | return True 88 | 89 | 90 | def example_generator(n): 91 | """Generators have a ``Yields`` section instead of a ``Returns`` section. 92 | 93 | Args: 94 | n (int): The upper limit of the range to generate, from 0 to `n` - 1 95 | 96 | Yields: 97 | int: The next number in the range of 0 to `n` - 1 98 | 99 | Examples: 100 | Examples should be written in doctest format, and should illustrate how 101 | to use the function. 102 | 103 | >>> print [i for i in example_generator(4)] 104 | [0, 1, 2, 3] 105 | 106 | """ 107 | for i in range(n): 108 | yield i 109 | 110 | 111 | class ExampleError(Exception): 112 | """Exceptions are documented in the same way as classes. 113 | 114 | The __init__ method may be documented in either the class level 115 | docstring, or as a docstring on the __init__ method itself. 116 | 117 | Either form is acceptable, but the two should not be mixed. Choose one 118 | convention to document the __init__ method and be consistent with it. 119 | 120 | Note: 121 | Do not include the `self` parameter in the ``Args`` section. 122 | 123 | Args: 124 | msg (str): Human readable string describing the exception. 125 | code (int, optional): Error code, defaults to 2. 126 | 127 | Attributes: 128 | msg (str): Human readable string describing the exception. 129 | code (int): Exception error code. 130 | 131 | """ 132 | def __init__(self, msg, code=2): 133 | self.msg = msg 134 | self.code = code 135 | 136 | 137 | class ExampleClass(object): 138 | """The summary line for a class docstring should fit on one line. 139 | 140 | If the class has public attributes, they should be documented here 141 | in an ``Attributes`` section and follow the same formatting as a 142 | function's ``Args`` section. 143 | 144 | Attributes: 145 | attr1 (str): Description of `attr1`. 146 | attr2 (list of str): Description of `attr2`. 147 | attr3 (int): Description of `attr3`. 148 | 149 | """ 150 | def __init__(self, param1, param2, param3=0): 151 | """Example of docstring on the __init__ method. 152 | 153 | The __init__ method may be documented in either the class level 154 | docstring, or as a docstring on the __init__ method itself. 155 | 156 | Either form is acceptable, but the two should not be mixed. Choose one 157 | convention to document the __init__ method and be consistent with it. 158 | 159 | Note: 160 | Do not include the `self` parameter in the ``Args`` section. 161 | 162 | Args: 163 | param1 (str): Description of `param1`. 164 | param2 (list of str): Description of `param2`. Multiple 165 | lines are supported. 166 | param3 (int, optional): Description of `param3`, defaults to 0. 167 | 168 | """ 169 | self.attr1 = param1 170 | self.attr2 = param2 171 | self.attr3 = param3 172 | 173 | def example_method(self, param1, param2): 174 | """Class methods are similar to regular functions. 175 | 176 | Note: 177 | Do not include the `self` parameter in the ``Args`` section. 178 | 179 | Args: 180 | param1: The first parameter. 181 | param2: The second parameter. 182 | 183 | Returns: 184 | True if successful, False otherwise. 185 | 186 | """ 187 | return True 188 | 189 | def __special__(self): 190 | """By default special members with docstrings are included. 191 | 192 | Special members are any methods or attributes that start with and 193 | end with a double underscore. Any special member with a docstring 194 | will be included in the output. 195 | 196 | This behavior can be disabled by changing the following setting in 197 | Sphinx's conf.py:: 198 | 199 | napoleon_include_special_with_doc = False 200 | 201 | """ 202 | pass 203 | 204 | def __special_without_docstring__(self): 205 | pass 206 | 207 | def _private(self): 208 | """By default private members are not included. 209 | 210 | Private members are any methods or attributes that start with an 211 | underscore and are *not* special. By default they are not included 212 | in the output. 213 | 214 | This behavior can be changed such that private members *are* included 215 | by changing the following setting in Sphinx's conf.py:: 216 | 217 | napoleon_include_private_with_doc = True 218 | 219 | """ 220 | pass 221 | 222 | def _private_without_docstring(self): 223 | pass 224 | -------------------------------------------------------------------------------- /commotion_client/utils/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 6 | This program is a part of The Commotion Client 7 | 8 | Copyright (C) 2014 Seamus Tuohy s2e@opentechinstitute.org 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License along with this program. If not, see . 21 | 22 | """ 23 | 24 | #TODO create seperate levels for the stream, the file, and the full logger 25 | from PyQt4 import QtCore 26 | import logging 27 | from logging import handlers 28 | import os 29 | import sys 30 | 31 | class LogHandler(object): 32 | """ 33 | Main logging controls for Commotion-Client. 34 | 35 | This application is ONLY to be called by the main application. This logger sets up the main namespace for all other logging to take place within. All other loggers should be the core string "commotion_client" and the packages __name__ to use inheretance from the main commotion package. This way the code in an indivdual extension will be small and will inheret the logging settings that were defined in the main application. 36 | 37 | Example Use for ALL other modules and packages: 38 | 39 | from commotion-client.utils import logger 40 | log = logger.getLogger("commotion_client"+__name__) 41 | 42 | 43 | NOTE: The exceptions in this function do not have translation implemented. This is that they are called before the QT application and, as such, are not pushed through QT's translation tools. This could be a mistake on the developers side, as he is a bit foggy on the specifics of QT translation. You can access the feature request at https://github.com/opentechinstitute/commotion-client/issues/24 44 | """ 45 | 46 | def __init__(self, name, verbosity=None, logfile=None): 47 | #set core logger 48 | self.logger = logging.getLogger(str(name)) 49 | self.logger.setLevel('DEBUG') 50 | #set defaults 51 | self.levels = {"CRITICAL":logging.CRITICAL, "ERROR":logging.ERROR, "WARN":logging.WARN, "INFO":logging.INFO, "DEBUG":logging.DEBUG} 52 | self.formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(lineno)d : %(message)s') 53 | self.stream = None 54 | self.file_handler = None 55 | self.logfile = None 56 | #setup logger 57 | self.set_logfile(logfile) 58 | self.set_verbosity(verbosity) 59 | 60 | def set_logfile(self, logfile=None): 61 | """Set the file to log to. 62 | 63 | Args: 64 | logfile (string): The absolute path to the file to log to. 65 | optional: defaults to the default system logfile path. 66 | """ 67 | if logfile: 68 | log_dir = QtCore.QDir(os.path.dirname(logfile)) 69 | if not log_dir.exists(): 70 | if log_dir.mkpath(log_dir.absolutePath()): 71 | self.logfile = logfile 72 | platform = sys.platform 73 | if platform == 'darwin': 74 | #Try /Library/Logs first 75 | log_dir = QtCore.QDir(os.path.join(QtCore.QDir.homePath(), "Library", "Logs")) 76 | #if it does not exist try and create it 77 | if not log_dir.exists(): 78 | if not log_dir.mkpath(log_dir.absolutePath()): 79 | raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.") 80 | self.logfile = log_dir.filePath("commotion.log") 81 | elif platform in ['win32', 'cygwin']: 82 | #Try ../AppData/Local/Commotion first 83 | log_dir = QtCore.QDir(os.path.join(os.getenv('APPDATA'), "Local", "Commotion")) 84 | #if it does not exist try and create it 85 | if not log_dir.exists(): 86 | if not log_dir.mkpath(log_dir.absolutePath()): 87 | raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '/.Commotion' does not exist and could not be created.") 88 | self.logfile = log_dir.filePath("commotion.log") 89 | elif platform == 'linux': 90 | #Try /var/logs/ 91 | log_dir = QtCore.QDir("/var/logs/") 92 | if not log_dir.exists(): #Seriously! What kind of twisted linux system is this? 93 | if log_dir.mkpath(log_dir.absolutePath()): 94 | self.logfile = log_dir.filePath("commotion.log") 95 | else: 96 | #If fail then just write logs in home directory 97 | #TODO check if this is appropriate... its not. 98 | home = QtCore.QDir.home() 99 | if not home.exists(".Commotion") and not home.mkdir(".Commotion"): 100 | raise NotADirectoryError("Attempted to set logging to the user's Commotion directory. The directory '{0}/.Commotion' does not exist and could not be created.".format(home.absolutePath())) 101 | else: 102 | home.cd(".Commotion") 103 | self.logfile = home.filePath("commotion.log") 104 | else: 105 | self.logfile = log_dir.filePath("commotion.log") 106 | else: 107 | #I'm out! 108 | raise OSError("Could not create a logfile.") 109 | 110 | def set_verbosity(self, verbosity=None, log_type=None): 111 | """Set's the verbosity of the logging for the application. 112 | 113 | Args: 114 | verbosity (string|int): The verbosity level for logging to take place. 115 | optional: Defaults to "Error" level 116 | log_type (string): The type of logging whose verbosity is to be changed. 117 | optional: If not specified ALL logging types will be changed. 118 | 119 | Returns: 120 | bool True if successful, False if failed 121 | 122 | Raises: 123 | exception: Description. 124 | 125 | """ 126 | try: 127 | int_level = int(verbosity) 128 | except ValueError: 129 | if str(verbosity).upper() in self.levels.keys(): 130 | level = self.levels[str(verbosity).upper()] 131 | else: 132 | return False 133 | else: 134 | if 1 <= int_level <= 5: 135 | _levels = [ 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG'] 136 | level = self.levels[_levels[int_level-1]] 137 | else: 138 | return False 139 | 140 | if log_type == "stream": 141 | set_stream = True 142 | elif log_type == "logfile": 143 | set_logfile = True 144 | else: 145 | set_logfile = True 146 | set_stream = True 147 | 148 | if set_stream == True: 149 | self.logger.removeHandler(self.stream) 150 | self.stream = None 151 | self.stream = logging.StreamHandler() 152 | self.stream.setFormatter(self.formatter) 153 | self.stream.setLevel(level) 154 | self.logger.addHandler(self.stream) 155 | if set_logfile == True: 156 | self.logger.removeHandler(self.file_handler) 157 | self.file_handler = None 158 | self.file_handler = handlers.RotatingFileHandler(self.logfile, 159 | maxBytes=5000000, 160 | backupCount=5) 161 | self.file_handler.setFormatter(self.formatter) 162 | self.file_handler.setLevel(level) 163 | self.logger.addHandler(self.file_handler) 164 | return True 165 | 166 | def get_logger(self): 167 | return self.logger 168 | -------------------------------------------------------------------------------- /commotion_client/GUI/crash_report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 6 | crash_reporter 7 | 8 | The crash handler for the gui components of the commotion client. 9 | 10 | Key components handled within. 11 | * creation of the crash report #TODO 12 | * loading the crash-report window #TODO 13 | * sending crash report when internet is available #TODO 14 | 15 | """ 16 | 17 | #Standard Library Imports 18 | import logging 19 | import traceback 20 | import uuid 21 | 22 | #PyQt imports 23 | from PyQt4 import QtCore 24 | from PyQt4 import QtGui 25 | 26 | from commotion_client.GUI.ui import Ui_crash_report_window 27 | 28 | class CrashReport(Ui_crash_report_window.crash_window): 29 | 30 | #A signal userd to alert the crash reporter to create a crash report window and await report information. 31 | crash_override = QtCore.pyqtSignal() 32 | crash_info = QtCore.pyqtSignal(str, dict) #generate a signal process that can accept a string containing the module name and a dict of the report data 33 | crash = QtCore.pyqtSignal(str) 34 | alert_user = QtCore.pyqtSignal(str) 35 | 36 | def __init__(self, parent=None): 37 | super().__init__() 38 | self.log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. 39 | self.setupUi(self) #run setup function from ui_crash_window 40 | self.create_uuid() #create uuid for this crash report instance 41 | self.report_timer = QtCore.QTimer() 42 | #if alert_user signal received show the crash window 43 | self.alert_user.connect(self.crash_alert) 44 | 45 | self.send_report.clicked.connect(self.generate_report) 46 | 47 | #Capture Quit and Reset Buttons 48 | self.restart_button.clicked.connect(self.check_restart) 49 | self.quit_button.clicked.connect(self.check_quit) 50 | 51 | def crash_alert(self, error): 52 | if error: 53 | self.error_text.setText(error) 54 | self.error_msg = error 55 | self.retranslateUi(self) 56 | 57 | self.crash_override.emit() 58 | self.exec_() 59 | 60 | def check_restart(self): 61 | if self.send_report.isChecked(): 62 | self.save_report() 63 | self.crash.emit("restart") 64 | 65 | def check_quit(self): 66 | if self.send_report.isChecked(): 67 | self.save_report() 68 | self.crash.emit("quit") 69 | 70 | def generate_report(self): 71 | #Check if report is already generated 72 | self.gatherer = ReportGatherer(self) 73 | 74 | #Add initial error 75 | if self.error_msg: 76 | self.gatherer.add_item("error", {"error":self.error_msg}) 77 | 78 | #connect the add_report signal to the report gatherer's add report function. 79 | #called from mainWindow as: self.SUBSECTION.crashReport.connect(self.CrashReport.add_report) 80 | self.crash_info.connect(self.gatherer.add_item) 81 | 82 | #TODO: Meaure how long it takes to return reports from various numbers of functions and modules to see how long this countdown should be (currently 5 seconds). 83 | self.countdown = 5 84 | self.countdown_timer = QtCore.QTimer() 85 | self.countdown_timer.timeout.connect(self.update_countdown) 86 | self.countdown_timer.start() 87 | 88 | def update_countdown(self): 89 | """ 90 | Slot for countdown timer timeout that populates the graphical countdown if required. 91 | """ 92 | self.countdown -= 1 93 | self.report_gen_countdown.setProperty("intValue", self.countdown) 94 | if self.countdown <= 0: 95 | self.countdown_timer.stop() 96 | self.report_loading_label.hide() 97 | self.report_gen_countdown.hide() 98 | self.set_report() #set report upon completion 99 | 100 | 101 | def set_report(self): 102 | """ 103 | set_report creates and saves the current error report and then disconnects crash_info signal. 104 | """ 105 | #turn off report gatherer 106 | self.crash_info.disconnect() 107 | try: 108 | self.compiled_report = self.gatherer.get_report() 109 | except Exception as e: 110 | self.log.error(QtCore.QCoreApplication.translate("logs", "Faile to create a crash report.")) 111 | self.log.debug(e, exc_info=1) 112 | self.crash_report.setPlainText(QtCore.QCoreApplication.translate("A crash report could not be generated.")) 113 | return 114 | else: 115 | #send to user if window activated 116 | printable_report = [] 117 | try: 118 | for section, results in self.compiled_report.items(): 119 | printable_report.append("\n========== "+section+" ==========\n") 120 | #format and append each name-value pair to the report. 121 | printable_report.append("\n".join(['%s = %s' %(name, value) for name, value in results.items()])) 122 | except Exception as e: 123 | self.log.error(QtCore.QCoreApplication.translate("logs", "Failed to format crash report for user to view.")) 124 | self.log.debug(e, exc_info=1) 125 | self.crash_report.setPlainText(QtCore.QCoreApplication.translate("Unable to parse crash report for viewing. You can send the report without viewing it or un-check \"Send crash report\" to cancel sending this report.")) 126 | else: 127 | self.crash_report.setPlainText("\n".join(printable_report)) 128 | #todo add logs 129 | 130 | def save_report(self): 131 | """ 132 | TODO ADD python-gnupg encryption to all data saved here. 133 | """ 134 | #Save user comments 135 | try: 136 | self.compiled_report['error']['comments'] = str(self.comment_field.toPlainText()) 137 | except Exception as e: 138 | self.log.info(QtCore.QCoreApplication.translate("logs", "The crash reporter could not store user comments in the crash report.")) 139 | self.log.debug(e, exc_info=1) 140 | _settings = QtCore.QSettings() 141 | _settings.beginGroup("CrashReport/"+self.uuid) #create a unique crash report 142 | for section, results in self.compiled_report.items(): 143 | for name, value in results.items(): 144 | _settings.setValue(section+"/"+name, value) 145 | _settings.endGroup() 146 | 147 | def create_uuid(self): 148 | dash_map = str.maketrans({"-":None}) #create a map of the dash char 149 | self.uuid = str.translate(str(uuid.uuid1()), dash_map) #create a uuid and remove dashes 150 | 151 | 152 | class ReportGatherer(): 153 | 154 | def __init__(self, parent=None): 155 | self.report = {} 156 | super().__init__() 157 | self.log = logging.getLogger("commotion_client."+__name__) #TODO commotion_client is still being called directly from one level up so it must be hard coded as a sub-logger if called from the command line. 158 | 159 | def get_report(self): 160 | #Add system info into report 161 | try: 162 | self.report['system'] = self.get_defaults() 163 | except Exception as e: 164 | self.log.warn(QtCore.QCoreApplication.translate("logs", "Could not add system information into the crash report.")) 165 | self.log.debug(e, exc_info=1) 166 | return self.report 167 | 168 | def add_item(self, name, item): 169 | try: 170 | self.report[name] = item 171 | except Exception as e: 172 | self.log.warn(QtCore.QCoreApplication.translate("logs", "The crash reporter could not add data from {0} into the crash report.").format(name)) 173 | self.log.debug(e, exc_info=1) 174 | 175 | def get_defaults(self): 176 | system_values = {} 177 | #get current app instance and the application version set there 178 | system_values['version'] = QtGui.QApplication.instance().applicationVersion() 179 | 180 | if QtCore.QSysInfo.ByteOrder == 0: 181 | system_values['endian'] = "big endian" 182 | else: 183 | system_values['endian'] = "little endian" 184 | system_values['architecture'] = str(QtCore.QSysInfo.WordSize)+"bit" 185 | 186 | return system_values 187 | -------------------------------------------------------------------------------- /commotion_client/GUI/ui/crash_report_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | crash_window 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 472 13 | 523 14 | 15 | 16 | 17 | Commotion Error 18 | 19 | 20 | 21 | :/alert32.png:/alert32.png 22 | 23 | 24 | 25 | 26 | 10 27 | 10 28 | 451 29 | 501 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | :/alert48.png 42 | 43 | 44 | 45 | 46 | 47 | 48 | Sorry! 49 | 50 | 51 | false 52 | 53 | 54 | true 55 | 56 | 57 | 0 58 | 59 | 60 | 61 | 62 | 63 | 64 | Qt::Horizontal 65 | 66 | 67 | QSizePolicy::MinimumExpanding 68 | 69 | 70 | 71 | 10 72 | 20 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 6 83 | 84 | 85 | 86 | 87 | 88 | 420 89 | 16777215 90 | 91 | 92 | 93 | Commotion has experienced an unknown error. 94 | 95 | 96 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 97 | 98 | 99 | 1 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 420 112 | 16777215 113 | 114 | 115 | 116 | We have created a crash report to allow the Commotion team to identify and address this problem. If you would be willing to be contated directly please include your e-mail in the comment section below. 117 | 118 | 119 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 120 | 121 | 122 | true 123 | 124 | 125 | 1 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | Send a report to the commotion team. 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 220 144 | 145 | 146 | 147 | 1 148 | 149 | 150 | 151 | Add a Comment 152 | 153 | 154 | 155 | 156 | 20 157 | 10 158 | 391 159 | 161 160 | 161 | 162 | 163 | 164 | 391 165 | 16777215 166 | 167 | 168 | 169 | 170 | 171 | 172 | View Crash Report 173 | 174 | 175 | 176 | 177 | 20 178 | 10 179 | 391 180 | 161 181 | 182 | 183 | 184 | 185 | 391 186 | 16777215 187 | 188 | 189 | 190 | 191 | 192 | 193 | 130 194 | 50 195 | 160 196 | 80 197 | 198 | 199 | 200 | 201 | 202 | 203 | Generating Report... 204 | 205 | 206 | Qt::AlignCenter 207 | 208 | 209 | 210 | 211 | 212 | 213 | Qt::LeftToRight 214 | 215 | 216 | QFrame::NoFrame 217 | 218 | 219 | 0 220 | 221 | 222 | 1 223 | 224 | 225 | 5 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | Qt::Horizontal 240 | 241 | 242 | 243 | 40 244 | 20 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | Quit Commotion 253 | 254 | 255 | 256 | 257 | 258 | 259 | Restart Commotion 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | quit_button 274 | clicked() 275 | crash_window 276 | close() 277 | 278 | 279 | 295 280 | 485 281 | 282 | 283 | 459 284 | -16 285 | 286 | 287 | 288 | 289 | restart_button 290 | clicked() 291 | crash_window 292 | close() 293 | 294 | 295 | 334 296 | 489 297 | 298 | 299 | 412 300 | 14 301 | 302 | 303 | 304 | 305 | 306 | -------------------------------------------------------------------------------- /commotionc.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/python 2 | 3 | import dbus.mainloop.glib ; dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 4 | import fcntl 5 | import glob 6 | import hashlib 7 | import os 8 | import pprint 9 | import pyjavaproperties 10 | import random 11 | import re 12 | import socket 13 | import struct 14 | import subprocess 15 | import syslog 16 | import tempfile 17 | import time 18 | 19 | class CommotionCore(): 20 | 21 | def __init__(self, src='commotionc'): 22 | self.olsrdconf = '/etc/olsrd/olsrd.conf' 23 | self.profiledir = '/etc/commotion/profiles.d/' 24 | self.logname = src 25 | 26 | 27 | def _generate_ip(self, base, netmask, interface): 28 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #First two lines adapted from stackoverflow.com/questions/159137/getting-mac-address 29 | hwiddata = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('32s', interface[:15])) 30 | netmaskaddr = socket.inet_aton(netmask) 31 | baseaddr = socket.inet_aton(base) 32 | hwaddr = hwiddata[20:24] #Extract only the last four 1-byte hex values of the returned mac address 33 | finaladdr = [] 34 | for i in range (4): 35 | finaladdr.append((ord(hwaddr[i]) & ~ord(netmaskaddr[i])) | (ord(baseaddr[i]) & ord(netmaskaddr[i]))) 36 | return socket.inet_ntoa(''.join([chr(item) for item in finaladdr])) 37 | 38 | def log(self, msg): 39 | syslog.openlog(self.logname) 40 | syslog.syslog(msg) 41 | syslog.closelog() 42 | 43 | def getInterface(self, preferred=None): 44 | interface = None 45 | for wireless in glob.iglob('/sys/class/net/*/wireless'): #Reformat as a list comprehension 46 | driver = os.listdir(os.path.join('/sys/class/net', wireless.split('/')[4], 'device/driver/module/drivers')) 47 | if driver[0].split(':')[1] in ('ath5k', 'ath6kl', 'ath9k', 'ath9k_htc', 'b43', 'b43legacy', 'carl9170', 'iwlegacy', 'iwlwifi', 'mac80211_hwsim', 'orinoco', 'p54pci', 'p54spi', 'p54usb', 'rndis_wlan', 'rt61pci', 'rt73usb', 'rt2400pci', 'rt2500pci', 'rt2500usb', 'rt2800usb', 'rtl8187', 'wl1251', 'wl12xx', 'zd1211rw'): 48 | interface = wireless.split('/')[4] 49 | if preferred == interface: 50 | return preferred 51 | if preferred: 52 | if interface: 53 | self.log("WARNING: Specified interface " + preferred + " does not support cfg80211 (ibss encryption), or ibss mode, or both! Interface " + interface + " does, however. Consider changing the interface setting in /etc/commotion/commotionc.conf") 54 | else: 55 | self.log('WARNING: No available wireless interfaces have support for both ibss mode and cfg80211 (ibss encryption)') 56 | return preferred 57 | elif interface: 58 | self.log("Mesh-compatible interface found! (" + interface + ")") 59 | return interface 60 | else: 61 | self.log('WARNING: No available wireless interfaces have support for both ibss mode and cfg80211 (ibss encryption)') 62 | return wireless.split('/')[4] 63 | #interface = subprocess.check_output(['/sbin/iw', 'dev']).split() 64 | #interface = interface[interface.index('Interface') + 1] 65 | #subprocess.check_output(['iw', 'list']) 66 | 67 | def readProfile(self, profname): 68 | f = os.path.join(self.profiledir, profname + '.profile') 69 | p = pyjavaproperties.Properties() 70 | p.load(open(f)) 71 | profile = dict() 72 | profile['filename'] = f 73 | profile['mtime'] = os.path.getmtime(f) 74 | for k,v in p.items(): 75 | profile[k] = v 76 | for param in ('ssid', 'channel', 'ip', 'netmask', 'dns', 'ipgenerate'): ##Also validate ip, dns, bssid, channel? 77 | if param not in profile: 78 | self.log('Error in ' + f + ': missing or malformed ' + param + ' option') ## And raise some sort of error? 79 | if profile['ipgenerate'] in ('True', 'true', 'Yes', 'yes', '1'): # and not profile['randomip'] 80 | self.log('Randomly generating static ip with base ' + profile['ip'] + ' and subnet ' + profile['netmask']) 81 | profile['ip'] = self._generate_ip(profile['ip'], profile['netmask'], self.getInterface()) 82 | self.updateProfile(profname, {'ipgenerate': 'false', 'ip': profile['ip']}) 83 | if not 'bssid' in profile: #Include note in default config file that bssid parameter is allowed, but should almost never be used 84 | self.log('Generating BSSID from hash of ssid and channel') 85 | bssid = hashlib.new('md5', profile['ssid']).hexdigest()[:8].upper() + '%02d' %int(int(profile['channel'])/10) + '%02d' %int(int(profile['channel'])%10) 86 | profile['bssid'] = ':'.join(a+b for a,b in zip(bssid[::2], bssid[1::2])) 87 | 88 | conf = os.path.join(re.sub('(.*)\profiles.d.*', r'\1olsrd.d', self.profiledir), profname + '.conf') #Unify this syntax by making profiledir just /etc/commotion? 89 | if os.path.exists(conf): 90 | self.log('profile has custom olsrd.conf: "' + conf + '"') 91 | profile['conf'] = conf 92 | else: 93 | self.log('using built in olsrd.conf: "' + self.olsrdconf + '"') 94 | profile['conf'] = self.olsrdconf 95 | return profile 96 | 97 | 98 | def readProfiles(self): 99 | '''get all the available mesh profiles and return as a dict''' 100 | profiles = dict() 101 | self.log('Reading profiles:') 102 | for f in glob.glob(self.profiledir + '*.profile'): 103 | profname = os.path.split(re.sub('\.profile$', '', f))[1] 104 | self.log('reading profile: "' + f + '"') 105 | profile = self.readProfile(profname) 106 | self.log('adding "' + f + '" as profile "' + profile['ssid'] + '"') 107 | profiles[profile['ssid']] = profile 108 | return profiles 109 | 110 | 111 | def updateProfile(self, profname, params): 112 | self.log('Updating profile \"' + profname + '\" ') 113 | fn = os.path.join(self.profiledir, profname + '.profile') 114 | if not os.access(fn, os.W_OK): 115 | self.log('Unable to write to ' + fn + ', so \"' + profname + '\" was not updated') 116 | return 117 | savedsettings = [] 118 | fd = open(fn, 'r') 119 | for line in fd: 120 | savedsettings.append(line) 121 | for param, value in params.iteritems(): 122 | if re.search('^' + param + '=', savedsettings[-1]): 123 | savedsettings[-1] = (param + '=' + value + '\n') 124 | break 125 | fd.close() 126 | fd = open(fn, 'w') 127 | for line in savedsettings: 128 | fd.write(line) 129 | fd.close() 130 | 131 | 132 | def startOlsrd(self, interface, conf): 133 | '''start the olsrd daemon''' 134 | self.log('start olsrd: ') 135 | cmd = ['/usr/sbin/olsrd', '-i', interface, '-f', conf] 136 | self.log(" ".join([x for x in cmd])) 137 | p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, 138 | stderr=subprocess.PIPE) 139 | out, err = p.communicate() 140 | if out: 141 | self.log('stdout: ' + out) 142 | if err: 143 | self.log('stderr: ' + err) 144 | 145 | 146 | def stopOlsrd(self): 147 | '''stop the olsrd daemon''' 148 | self.log('stop olsrd') 149 | cmd = ['/usr/bin/killall', '-v', 'olsrd'] 150 | self.log(" ".join([x for x in cmd])) 151 | p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, 152 | stderr=subprocess.PIPE) 153 | out, err = p.communicate() 154 | if out: 155 | self.log('stdout: ' + out) 156 | if err: 157 | self.log('stderr: ' + err) 158 | 159 | 160 | def _create_wpasupplicant_conf(self, profile, tmpfd): 161 | contents = [] 162 | contents.append('ap_scan=2\n') 163 | contents.append('network={\n') 164 | contents.append('\tmode=1\n') 165 | contents.append('\tssid' + '=' + '\"' + profile['ssid'] + '\"\n') 166 | contents.append('\tbssid' + '=' + profile['bssid'] + '\n') 167 | contents.append('\tfrequency' + '=' + str((int(profile['channel']))*5 + 2407) + '\n') 168 | if 'psk' in profile: 169 | contents.append('\tkey_mgmt=WPA-PSK' + '\n') 170 | contents.append('\tpsk' + '=' + '\"' + profile['psk'] + '\"\n') 171 | contents.append('}') 172 | for line in contents: 173 | tmpfd.write(line) 174 | 175 | def fallbackConnect(self, profileid): 176 | profile = self.readProfile(profileid) 177 | interface = self.getInterface() 178 | ip = profile['ip'] 179 | if 'connected' in subprocess.check_output(['/usr/bin/nmcli', 'nm', 'status']): #Connected in this context means "active," not just "connected to a network" 180 | self.log('Putting network manager to sleep...') 181 | try: 182 | subprocess.check_call(['/usr/bin/nmcli', 'nm', 'sleep', 'true']) 183 | except: 184 | self.log('Error putting network manager to sleep!') 185 | self.log('Killing default version of wpa_supplicant...') 186 | try: 187 | subprocess.check_call(['/usr/bin/pkill', '-9', 'wpa_supplicant']) 188 | except: 189 | self.log('Error killing wpa_supplicant!') 190 | 191 | self.log('Bringing ' + interface + ' down...') 192 | try: 193 | subprocess.check_call(['/sbin/ifconfig', interface, 'down']) 194 | except: 195 | self.log('Error bringing interface down!') 196 | ##Check for existance of replacement binary 197 | self.log('Starting replacement wpa_supplicant with profile ' + profileid + ', interface ' + interface + ', and ip address ' + ip + '.') 198 | wpasupplicantconf = tempfile.NamedTemporaryFile('w+b', 0) 199 | self._create_wpasupplicant_conf(profile, wpasupplicantconf) 200 | subprocess.Popen(['/usr/bin/commotion_wpa_supplicant', '-Dnl80211', '-i' + interface, '-c' + wpasupplicantconf.name]) 201 | time.sleep(2) 202 | wpasupplicantconf.close() 203 | try: 204 | subprocess.check_call(['/sbin/ifconfig', interface, 'up', ip, 'netmask', '255.0.0.0']) 205 | except: 206 | self.log('Error bringing interface up!') 207 | 208 | self.startOlsrd(interface, profile['conf']) 209 | 210 | -------------------------------------------------------------------------------- /commotion_client/GUI/main_window.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | main_window 6 | 7 | The main window and system tray for the commotion_client pyqt GUI. 8 | 9 | Key componenets handled within: 10 | * exiting/hiding the application 11 | * Creating the main window and systemTray icon. 12 | 13 | """ 14 | #Standard Library Imports 15 | import logging 16 | 17 | #PyQt imports 18 | from PyQt4 import QtCore 19 | from PyQt4 import QtGui 20 | 21 | #Commotion Client Imports 22 | import commotion_assets_rc 23 | from commotion_client.GUI.menu_bar import MenuBar 24 | from commotion_client.GUI.crash_report import CrashReport 25 | from commotion_client.GUI import welcome_page 26 | from commotion_client.GUI import toolbar_builder 27 | from commotion_client.utils import extension_manager 28 | 29 | 30 | class MainWindow(QtGui.QMainWindow): 31 | """ 32 | The central widget for the commotion client. This widget initalizes all other sub-widgets and extensions as well as defines the paramiters of the main GUI container. 33 | """ 34 | 35 | #Clean up signal atched by children to do any clean-up or saving needed 36 | clean_up = QtCore.pyqtSignal() 37 | app_message = QtCore.pyqtSignal(str) 38 | 39 | def __init__(self, parent=None): 40 | super().__init__() 41 | #Keep track of if the gui needs any clean up / saving. 42 | self._dirty = False 43 | self.log = logging.getLogger("commotion_client."+__name__) 44 | self.translate = QtCore.QCoreApplication.translate 45 | 46 | self.init_crash_reporter() 47 | self.setup_menu_bar() 48 | #Setup extension manager for viewports 49 | self.ext_manager = extension_manager.ExtensionManager() 50 | self.viewport = welcome_page.ViewPort 51 | self.apply_viewport(self.viewport) 52 | 53 | #Default Paramiters #TODO to be replaced with paramiters saved between instances later 54 | try: 55 | self.load_settings() 56 | except Exception as _excp: 57 | self.log.critical(self.translate("logs", "Failed to load window settings.")) 58 | self.log.exception(_excp) 59 | raise 60 | 61 | #set main menu to not close application on exit events 62 | self.exitOnClose = False 63 | self.remove_on_close = False 64 | 65 | #================================== 66 | 67 | def toggle_menu_bar(self): 68 | #if menu shown... then 69 | #DockToHide = self.findChild(name="MenuBarDock") 70 | #QMainWindow.removeDockWidget (self, QDockWidget dockwidget) 71 | #else 72 | #bool QMainWindow.restoreDockWidget (self, QDockWidget dockwidget) 73 | pass 74 | 75 | def setup_menu_bar(self): 76 | """ Set up menu bar. """ 77 | self.menu_bar = MenuBar(self) 78 | #Create dock for menu-bar TEST 79 | self.menu_dock = QtGui.QDockWidget(self) 80 | #turn off title bar 81 | #TODO create a vertical title bar that is the "dock handle" 82 | self.menu_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) 83 | #Set Name of dock so we can hide and show it. 84 | self.menu_dock.setObjectName("MenuBarDock") 85 | #force bar to the left side 86 | self.menu_dock.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea) 87 | #apply menu bar to dock and dock to the main window 88 | self.menu_dock.setWidget(self.menu_bar) 89 | self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.menu_dock) 90 | 91 | #Create slot to monitor when menu-bar wants the main window to change the main-viewport 92 | self.menu_bar.viewport_requested.connect(self.change_viewport) 93 | 94 | def init_crash_reporter(self): 95 | """ """ 96 | try: 97 | self.crash_report = CrashReport() 98 | except Exception as _excp: 99 | self.log.critical(self.translate("logs", "Failed to load crash reporter. Ironically, this means that the application must be halted.")) 100 | self.log.exception(_excp) 101 | raise 102 | else: 103 | self.crash_report.crash.connect(self.crash) 104 | 105 | def set_viewport(self): 106 | """Load and set viewport to next viewport and load viewport """ 107 | self.log.info(self.next_extension) 108 | next_view = self.next_extension 109 | ext_viewport = self.ext_manager.load_user_interface(str(next_view), "main") 110 | ext_toolbar = self.ext_manager.load_user_interface(str(next_view), "toolbar") 111 | self.apply_viewport(ext_viewport, ext_toolbar) 112 | 113 | def apply_viewport(self, viewport, toolbar=None): 114 | """Apply current viewport to the central widget and set up proper signal's for communication. """ 115 | #Create central widget (replaced due to splitter) 116 | # self.central_widget = QtGui.QWidget(self) 117 | self.central_widget = QtGui.QSplitter(QtCore.Qt.Vertical, self) 118 | self.viewport = viewport(self.central_widget) 119 | if not toolbar: 120 | toolbar = False 121 | self.toolbar = self.init_toolbar(toolbar) 122 | 123 | #Set up central layout (Replaced due to splitter) 124 | #self.central_layout = QtGui.QVBoxLayout(self.central_widget) 125 | 126 | self.scroll_area = QtGui.QScrollArea(self.central_widget) 127 | self.scroll_area.setWidgetResizable(True) 128 | self.scroll_area.setWidget(self.viewport) 129 | 130 | #add scroll area to central layout (replaced due to splitter) 131 | #self.central_layout.addWidget(self.scroll_area) 132 | 133 | self.central_widget.addWidget(self.scroll_area) 134 | self.central_widget.addWidget(self.toolbar) 135 | 136 | self.setCentralWidget(self.central_widget) 137 | self.init_viewport_signals() 138 | self.central_widget.show() 139 | self.viewport.show() 140 | 141 | def init_viewport_signals(self): 142 | #connect viewport extension to crash reporter 143 | self.viewport.data_report.connect(self.crash_report.crash_info) 144 | self.crash_report.crash_override.connect(self.viewport.start_report_collection) 145 | 146 | #connect error reporter to crash reporter 147 | self.viewport.error_report.connect(self.crash_report.alert_user) 148 | 149 | #Attach clean up signal 150 | self.clean_up.connect(self.viewport.clean_up) 151 | 152 | def change_viewport(self, viewport): 153 | """Prepare next viewport for loading and start loading process when ready.""" 154 | self.log.debug(self.translate("logs", "Request to change viewport received.")) 155 | self.next_extension = viewport 156 | if self.viewport.is_dirty: 157 | self.viewport.on_stop.connect(self.set_viewport) 158 | self.clean_up.emit() 159 | else: 160 | self.set_viewport() 161 | 162 | def init_toolbar(self, ext_toolbar): 163 | """ """ 164 | toolbar = toolbar_builder.ToolBar(self.central_widget, self.viewport, ext_toolbar,) 165 | return toolbar 166 | 167 | 168 | def purge(self): 169 | """ 170 | Closes the menu and sets its data up for immediate removal. 171 | """ 172 | self.cleanup() 173 | self.main.remove_on_close = True 174 | self.close() 175 | 176 | 177 | def closeEvent(self, event): 178 | """ 179 | Captures the close event for the main window. When called from exitEvent removes a trayIcon and accepts its demise. When called otherwise will simply hide the main window and ignore the event. 180 | """ 181 | 182 | if self.exitOnClose: 183 | self.log.debug(self.translate("logs", "Application has received a EXIT close event and will shutdown completely.")) 184 | event.accept() 185 | elif self.remove_on_close: 186 | self.log.debug(self.translate("logs", "Application has received a GUI closing close event and will close its main window.")) 187 | self.deleteLater() 188 | event.accept() 189 | else: 190 | self.log.debug(self.translate("logs", "Application has received a non-exit close event and will hide its main window.")) 191 | self.hide() 192 | event.setAccepted(True) 193 | event.ignore() 194 | 195 | def exitEvent(self): 196 | """ 197 | Closes and exits the entire commotion program. 198 | """ 199 | self.cleanup() 200 | self.exitOnClose = True 201 | self.close() 202 | 203 | def cleanup(self): 204 | self.clean_up.emit() #send signal for others to clean up if they need to 205 | if self.is_dirty: 206 | self.save_settings() 207 | 208 | 209 | def bring_front(self): 210 | """ 211 | Brings the main window to the front of the screen. 212 | """ 213 | self.show() 214 | self.raise_() 215 | 216 | def load_settings(self): 217 | """ 218 | Loads window geometry from saved settings and sets window to those settings. 219 | """ 220 | defaults = { 221 | #QRect(posX, posY, width, height) 222 | "geometry":QtCore.QRect(300, 300, 640, 480), #TODO set sane defaults and catalogue in HIG 223 | } 224 | 225 | _settings = QtCore.QSettings() 226 | _settings.beginGroup("MainWindow") 227 | 228 | #Load settings from saved, or use defaults 229 | geometry = _settings.value("geometry", defaults['geometry']) 230 | if geometry.isNull() == True: 231 | _error = self.translate("logs", "Could not load window geometry from settings file or defaults.") 232 | self.log.critical(_error) 233 | raise EnvironmentError(_error) 234 | _settings.endGroup() 235 | self.setGeometry(geometry) 236 | 237 | def save_settings(self): 238 | """ 239 | Saves current window geometry 240 | """ 241 | 242 | _settings = QtCore.QSettings() 243 | _settings.beginGroup("MainWindow") 244 | #Save settings 245 | try: 246 | _settings.setValue("geometry", self.geometry()) 247 | except Exception as _excp: 248 | self.log.warn(self.translate("logs", "Could not save window geometry. Will continue without saving window geometry.")) 249 | self.log.exception(_excp) 250 | _settings.endGroup() 251 | 252 | 253 | def crash(self, crash_type): 254 | """ 255 | Emits a closing signal to allow other windows who need to clean up to clean up and then exits the application. 256 | """ 257 | self.clean_up.emit() #send signal for others to clean up if they need to 258 | if crash_type == "restart": 259 | self.app_message.emit("restart") 260 | else: 261 | self.exitOnClose = True 262 | self.close() 263 | 264 | @property 265 | def is_dirty(self): 266 | """Get the current state of the main window""" 267 | return self._dirty 268 | 269 | 270 | -------------------------------------------------------------------------------- /docs/extensions/writing_extensions.md: -------------------------------------------------------------------------------- 1 | # Writing Extensions 2 | 3 | ## Intro 4 | 5 | This documentation will walk you through how to create a Commotion Client extension using the provided templates and Qt Designer. This documentation follows the development of the core extension responsable for managing Commotion configuration files. You can find the code for the version of the extension built in this tutorial in the "docs/extensions/tutorial" folder. The current Commotion config extension can be found in "commotion_client/extension/core/config_manager." This tutorial will not keep up to date with this extension as it evolves unless there are core changes in the commotion API that require us to update sections to maintain current with the API. 6 | 7 | ## Design 8 | 9 | Commotion comes with a [JSON](http://json.org/) based network configuration file. This file contains the mesh settings for a Commotion network. These profiles currently contain the following values. 10 | ``` 11 | { 12 | "announce": "true", 13 | "bssid": "02:CA:FF:EE:BA:BE", 14 | "bssidgen": "true", 15 | "channel": "5", 16 | "dns": "208.67.222.222", 17 | "domain": "mesh.local", 18 | "encryption": "psk2", 19 | "ip": "100.64.0.0", 20 | "ipgen": "true", 21 | "ipgenmask": "255.192.0.0", 22 | "key": "c0MM0t10n!r0cks", 23 | "mdp_keyring": "/etc/commotion/keys.d/mdp.keyring/serval.keyring", 24 | "mdp_sid": "0000000000000000000000000000000000000000000000000000000000000000", 25 | "mode": "adhoc", 26 | "netmask": "255.192.0.0", 27 | "serval": "false", 28 | "ssid": "commotionwireless.net", 29 | "type": "mesh" 30 | "routing": "olsr" 31 | "family":"1pv4" 32 | } 33 | ``` 34 | 35 | Any good user interface starts with a need and a user needs assessment. We *need* an interface that will allow a user to understand, and edit a configuration file. Our initial *user needs assessment* revealed two groups of users. 36 | 37 | Basic Users: 38 | * These users want to be able to download, or be given, a config file and use it to connect to a network with the least ammount of manipulation. 39 | * These users want interfaces based upon tasks, not configuration files, when they do modify their settings. 40 | * When these users download a configuration file they want it to be named somthing that allows it to be easy to identify later/ 41 | 42 | Intermediate/Advanced Users 43 | * These users desire all the attributes that are listed under the *basic user.* 44 | * These users also want an interface where they can quickly manipulate all the network settings quickly without having to worry about abstractions that have been layered on for new users. 45 | * These abstractions make advanced users feel lost and frustrated because they know what they want to do, but can not find the "user friendly" term that pairs with the actual device behavior. 46 | 47 | 48 | Our needs assessment identified two different extensions. One extension is a device configuration interface that abstracts individual networking tasks into their component parts for easy configuration by a new user. The second extension is a configuration file loader, downloader, editor, and applier. This second extension is what we will build here. 49 | 50 | The Commotion [Human Interface Guidelines](http://commotionwireless.net/developer/hig/key-concepts) have some key concepts for user interface that we should use to guide our page design. 51 | 52 | **Common Language:** The Commotion config file uses a series of abbreviations for each section. Because this menu is focused on more advanced users we should provide not only the abbreviation, but the technical term for the object that any value interacts with. 53 | 54 | **Common UI Terms:** Beyond simply the config file key, and the true technical term, as new users start to interact more competantly with Commotion it will be confusing if the common terms we use in the basic interfaces is not also included. As such we will want to use the *common term* where one exists. 55 | 56 | Taking just one value we can sketch out how the interface will represent it. 57 | 58 | ```"ssid": "commotionwireless.net"``` 59 | 60 | ![A sketch of a possible SSID including all terms.](tutorial/images/design/ssid_sketch.png) 61 | 62 | Beyond the consitancy provided by common terms, common groupings are also important. In order to ensure that a user can easily modify related configurations. We have grouped the configuration values in the following two groups. 63 | 64 | ``` 65 | security { 66 | "announce" 67 | "encryption" 68 | "key 69 | "serval" 70 | "mdp_keyring" 71 | "mdp_sid" 72 | } 73 | 74 | networking { 75 | "routing" 76 | "mode" 77 | "type" 78 | "channel" 79 | "ssid" 80 | "bssidgen" 81 | "bssid" 82 | "domain" 83 | "family" 84 | "netmask" 85 | "ip" 86 | "ipgen" 87 | "ipgenmask" 88 | "dns" 89 | } 90 | ``` 91 | 92 | TODO: 93 | * Show process of designing section headers 94 | * Show final design 95 | 96 | ## The Qt Designer 97 | 98 | Now that we have the basic layout we can go to Qt Designer and create our page. Qt Designer is far more full featured than what we will cover here. A quick online search will show you far better demonstrations than I can give. Also, showing off Qt is not the focus of this tutorial. 99 | 100 | First we create a new dialogue. Since our design was a series of sections that are filled with a variety of values I am going to start by creating a single section title. 101 | 102 | Using our design document I know that I want the section header to be about 15px and bold. I can do this in a few ways. I can set the font directly in the "property editor", use the "property editor" to create a styleSheet to apply to the section header, or use an existing style sheet using the "Add Resource" button in the styleSheet importer or in the code. I recoomend the last option because you can use existing Commotion style sheets to make your extension fit the overall style of the application. The "Main" section will have instructions on how to apply a existing Commotion style sheet to your GUI. 103 | 104 | 105 | 106 | Feel free to use whatever works for you. To make it easy for me to create consistant styling later I am just going to do everything unstyled in the Qt Designer and then call in a style-sheet in the "Main" section. 107 | 108 | Not that I have my section header I am going copy it three times and set the "objectName" and the user-facing text of each. Once this extension is functional I will go back through and replace all the text with user-tested text. For now, lets just get it working. 109 | 110 | 111 | 112 | Now that are section headers are created we are goign to have to go in and create our values. Qt Designer has a variety of widgets that you can choose from. I am only going to go over the creation of one widget to show you how to put them together. 113 | 114 | First we are going to make a simple text-entry field following the design we created before. The following was created using four "label's" and one "line edit" box. 115 | 116 | 117 | 118 | In the end, I won't be using labels for the help-text pop-up or the question mark. I realized that the easiest way to show the help-text pop up was to simply use the question-marks existing tool-tip object. There are two ways I could have implemented the question-mark icon. 119 | 120 | I can use a label for the question mark icon because non-interactive icons are easily made into graphics by using the "property editor." In the QLabel section click the "pixmap" property and choose "Choose Resource" in the dropdown menu to the right. This will allow you to choose a resource to use for your image. You can choose the "commotion_client/assets/commotion_assets.qrc" file to use any of the standard Commotion icons. We have tried to make sure that we have any sizes you might need of our standard icons. 121 | 122 | I would like the question mark to be clickable to make the help-text tooltip pop up without forcing the user to wait. To do this I added a push button, chose the same question mark item for its "icon," set its text to be blank, and checked the "flat" attribute. 123 | 124 | Now that we have the objects needed for the value we will use a layout to place them next to each other. I will use a horizontal layout to place the value header and the question mark next to each other. I have placed a "horizontal spacer" between them to push them to the edges of the horizontal layout. After copying my value header I am going to place it, the text entry box, and the static help text in a vertical layout. 125 | 126 | Not that I have the basic components created I am going to create the first value that I need with a text-box entry form, BSSID. After copying the object, the first thing I did was change the objectName of each element to reflect its value. I already have text for this field so I added it where appropriate. For the tooltip, I clicked on the question mark button and and edited the toolTip text. 127 | 128 | After completing this widget I copied it for every text box, and used its parts to construct all the other widgets I needed. Once each set of values in a section were complete I select the section header and all section values and use the "Lay Out Vertcally" button to place them in a singular layout. I then went through and named all the layouts to accurately reflect the contents inside of them. 129 | 130 | 131 | Once I have completed all the values I have to add the finishing touches before I can connect this to its backend. By clicking on some empty space outside of all my objects I selected the main window. I gave it the "objectName" ViewPort, a "windowIcon" form the commotion assets, and a "windowTitle." Now that the window is also set I can save this object and load it up in my back-end. 132 | 133 | This object saves as a ui file. If you are developing from within the commotion_clients repository you can use the existing makefile with the commant ```make test``` to have it compile your ui file into a python file you can sub-class from. It will also create a temporary compiled commotion_assets_rc.py file that will be needed to use any of the core Commotion assets without fully building the pyqt project. Once you have run ```make test``` A python file named "Ui_.py" will be created in the same directory as your .ui file. 134 | 135 | 136 | ## The Backend 137 | 138 | #### The Config 139 | 140 | Before the main window will load your application it needs a configuration file to load it from. This config file should be placed in your extensions main directory. For testing, you can place a copy of it in the folder "commotion_client/data/extensions/." The Commotion client will then automatically load your extension from its place in the "commotion_client/extensions/contrib" directory. We will cover how to package your extension for installation in the last section. 141 | 142 | Create a file in your main extension directory called ```config.json```. In that file place a json structure including the following items. 143 | ``` 144 | { 145 | "name":"config_manager", 146 | "menuItem":"Configuration Editor", 147 | "parent":"Advanced", 148 | "settings":"settings", 149 | "taskbar":"task_bar", 150 | "main":"main", 151 | "tests":"test_suite" 152 | } 153 | ``` 154 | The "taskbar," "tests," and "settings," values are optional. But we will be making them in this tutorial. You can find explanations of each value at https://wiki.commotionwireless.net/doku.php?id=commotion_architecture:commotion_client_architecture#extension_config_properties 155 | 156 | Once you have a config file in place we can actually create the logic behind our application. 157 | 158 | ### Main 159 | 160 | The main component of your extension is the "main" python file as identified by the config. This file should be placed in the root of your extension's directory structure. I reccomend starting from the "main.py" template in the "docs/extension_template" directory in the commotion structure. That is what I will be starting from here. 161 | 162 | #### Loading your extensions GUI 163 | 164 | First you wan't to import your extension's ui. If you are creating an add on you will use an import from the extensions directory using a style similar to ```from extensions.contrib..ui import Ui_```. Since I will be building a core extension I will be using the following statement. 165 | ``` 166 | from extensions.core.config_manager.ui import Ui_config_manager.py 167 | ``` 168 | 169 | Then you will extend the GUI that you created using the ViewPort class. 170 | 171 | ``` 172 | class ViewPort(Ui_config_manager.ViewPort): 173 | ``` 174 | 175 | If you are using the template the configuration is ready to apply stylesheets, and setup unit tests. 176 | 177 | #### Stylesheets 178 | 179 | Before we create our back-end we should finish the front end. The last step is going to be applying a style sheet to our object. I will quickly go over pyqt style sheets and then show you how to apply one of the default Commotion style sheets to your object. 180 | 181 | #### Unit Tests 182 | 183 | #### Taskbar 184 | 185 | ### Settings 186 | 187 | ### Packaging an extension 188 | 189 | 190 | -------------------------------------------------------------------------------- /commotion_client/utils/validate.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | #!/usr/bin/env python3 4 | # -*- coding: utf-8 -*- 5 | 6 | """ 7 | validate 8 | 9 | A collection of validation functions 10 | 11 | Key componenets handled within: 12 | 13 | """ 14 | #Standard Library Imports 15 | import logging 16 | import sys 17 | import re 18 | import ipaddress 19 | import os 20 | import zipfile 21 | 22 | #PyQt imports 23 | from PyQt4 import QtCore 24 | 25 | #Commotion Client Imports 26 | from commotion_client.utils import fs_utils 27 | 28 | class ClientConfig(object): 29 | 30 | def __init__(self, config, directory=None): 31 | """ 32 | Args: 33 | config (dictionary): The config for the extension. 34 | directory (string): Absolute Path to the directory containing the extension zipfile. If not specified the validator will ONLY check the validity of the config passed to it. 35 | """ 36 | self.config_values = ["name", 37 | "main", 38 | "menu_item", 39 | "menu_level", 40 | "parent", 41 | "settings", 42 | "toolbar", 43 | "tests", 44 | "initialized",] 45 | self.log = logging.getLogger("commotion_client."+__name__) 46 | self.translate = QtCore.QCoreApplication.translate 47 | self.config = config 48 | if directory: 49 | #set extension directory to point at config zipfile in that directory 50 | self.extension_path = directory 51 | self.errors = None 52 | 53 | @property 54 | def config(self): 55 | """Return the config value.""" 56 | return self._config 57 | 58 | @config.setter 59 | def config(self, value): 60 | """Check for valid values before allowing them to be set.""" 61 | if 'name' not in value: 62 | raise KeyError(self.translate("logs", "The config file must contain at least a name value.")) 63 | for val in value.keys(): 64 | if val not in self.config_values: 65 | raise KeyError(self.translate("logs", "The config file specified has the value {0} within it which is not a valid value.".format(val))) 66 | self._config = value 67 | 68 | 69 | @property 70 | def extension_path(self): 71 | return self._extension_path 72 | 73 | @extension_path.setter 74 | def extension_path(self, value): 75 | """Takes any directory passed to it and specifies the config file """ 76 | value_dir = QtCore.QDir(value) 77 | #Check that the directory in fact exists. 78 | if not value_dir.exists(): 79 | raise NotADirectoryError(self.translate("logs", "The directory should, by definition, actually be a directory. What was submitted was not a directory. Please specify the directory of an existing extension to continue.")) 80 | #Check that there are files in the directory provided 81 | if not value_dir.exists(self.config['name']): 82 | raise FileNotFoundError(self.translate("logs", "The extension is not in the extension directory provided. Is an extension directory without an extension an extension directory at all? We will ponder these mysteries while you check to see if the extension directory provided is correct." )) 83 | #Check that we can read the directory and its files. Sadly, QDir.isReadable() is broken on a few platforms so we check that and use the file filter to check each file. 84 | value_dir.setFilter(QtCore.QDir.Readable|QtCore.QDir.Files) 85 | file_list = value_dir.entryInfoList() 86 | if not file_list or not value_dir.isReadable(): 87 | raise PermissionError(self.translate("logs", "The application does not have permission to read any files within this directory. How is it supposed to validate the extension within then? You ask. It can't. Please modify the permissions on the directory and files within to allow the application to read the extension file.")) 88 | #Set the extension "directory" to point at the extension zipfile 89 | path = os.path.join(value, self.config['name']) 90 | self._extension_path = path 91 | 92 | def validate_all(self): 93 | """Run all validation functions on an uncompressed extension. 94 | 95 | @brief Will set self.errors if any errors are found. 96 | @return bool True if valid, False if invalid. 97 | """ 98 | self.errors = None 99 | if not self.config: 100 | raise NameError(self.translate("logs", "ClientConfig validator requires at least a config has been specified")) 101 | errors = [] 102 | if not self.name(): 103 | errors.append("name") 104 | self.log.info(self.translate("logs", "The name of extension {0} is invalid.".format(self.config['name']))) 105 | if not self.tests(): 106 | errors.append("tests") 107 | self.log.info(self.translate("logs", "The extension {0}'s tests is invalid.".format(self.config['name']))) 108 | if not self.menu_level(): 109 | errors.append("menu_level") 110 | self.log.info(self.translate("logs", "The extension {0}'s menu_level is invalid.".format(self.config['name']))) 111 | if not self.menu_item(): 112 | errors.append("menu_item") 113 | self.log.info(self.translate("logs", "The extension {0}'s menu_item is invalid.".format(self.config['name']))) 114 | if not self.parent(): 115 | errors.append("parent") 116 | self.log.info(self.translate("logs", "The extension {0}'s parent is invalid.".format(self.config['name']))) 117 | else: 118 | for gui_name in ['main', 'settings', 'toolbar']: 119 | if not self.gui(gui_name): 120 | self.log.info(self.translate("logs", "The extension {0}'s {1} is invalid.".format(self.config['name'], gui_name))) 121 | errors.append(gui_name) 122 | if errors: 123 | self.errors = errors 124 | return False 125 | else: 126 | return True 127 | 128 | 129 | def gui(self, gui_name): 130 | """Validate of one of the gui objects config values. (main, settings, or toolbar) 131 | 132 | @param gui_name string "main", "settings", or "toolbar" 133 | """ 134 | try: 135 | val = str(self.config[gui_name]) 136 | except KeyError: 137 | if gui_name != "main": 138 | try: 139 | val = str(self.config["main"]) 140 | except KeyError: 141 | val = str('main') 142 | else: 143 | val = str('main') 144 | file_name = val + ".py" 145 | if not self.check_path(file_name): 146 | self.log.warning(self.translate("logs", "The extensions {0} file name is invalid for this system.".format(gui_name))) 147 | return False 148 | if not self.check_exists(file_name): 149 | self.log.warning(self.translate("logs", "The extensions {0} file does not exist.".format(gui_name))) 150 | return False 151 | return True 152 | 153 | def name(self): 154 | try: 155 | name_val = str(self.config['name']) 156 | except KeyError: 157 | self.log.warning(self.translate("logs", "There is no name value in the config file. This value is required.")) 158 | return False 159 | if not self.check_path_length(name_val): 160 | self.log.warning(self.translate("logs", "This value is too long for your system.")) 161 | return False 162 | if not self.check_path_chars(name_val): 163 | self.log.warning(self.translate("logs", "This value uses invalid characters for your system.")) 164 | return False 165 | return True 166 | 167 | def menu_item(self): 168 | """Validate a menu item value.""" 169 | try: 170 | val = str(self.config["menu_item"]) 171 | except KeyError: 172 | if self.name(): 173 | val = str(self.config["name"]) 174 | else: 175 | self.log.warning(self.translate("logs", "The name value is the default for a menu_item if none is specified. You don't have a menu_item specified and the name value in this config is invalid.")) 176 | return False 177 | if not self.check_menu_text(val): 178 | self.log.warning(self.translate("logs", "The menu_item value is invalid")) 179 | return False 180 | return True 181 | 182 | def parent(self): 183 | """Validate a parent value.""" 184 | try: 185 | val = str(self.config["parent"]) 186 | except KeyError: 187 | self.log.info(self.translate("logs", "There is no 'parent' value set in the config. As such the default value of 'Extensions' will be used.")) 188 | return True 189 | if not self.check_menu_text(val): 190 | self.log.warning(self.translate("logs", "The parent value is invalid")) 191 | return False 192 | return True 193 | 194 | def menu_level(self): 195 | """Validate a Menu Level Config item.""" 196 | try: 197 | val = int(self.config["menu_level"]) 198 | except KeyError: 199 | self.log.info(self.translate("logs", "There is no 'menu_level' value set in the config. As such the default value of 10 will be used.")) 200 | return True 201 | except ValueError: 202 | self.log.info(self.translate("logs", "The 'menu_level' value set in the config is not a number and is therefore invalid.")) 203 | return False 204 | if not 0 < val > 100: 205 | self.log.warning(self.translate("logs", "The menu_level is invalid. Choose a number between 1 and 100")) 206 | return False 207 | return True 208 | 209 | def tests(self): 210 | """Validate a tests config menu item.""" 211 | try: 212 | val = str(self.config["tests"]) 213 | except KeyError: 214 | val = str('tests') 215 | file_name = val + ".py" 216 | if not self.check_path(file_name): 217 | self.log.warning(self.translate("logs", "The extensions 'tests' file name is invalid for this system.")) 218 | return False 219 | if not self.check_exists(file_name): 220 | self.log.info(self.translate("logs", "The extensions 'tests' file does not exist. But tests are not required. Shame on you though, SHAME!.")) 221 | return True 222 | 223 | def check_menu_text(self, menu_text): 224 | """ 225 | Checks that menu text fits within the accepted string length bounds. 226 | 227 | @param menu_text string The text that will appear in the menu. 228 | """ 229 | if not 3 < len(str(menu_text)) < 40: 230 | self.log.warning(self.translate("logs", "Menu items must be between 3 and 40 chars long. Becuase it looks prettier that way.")) 231 | return False 232 | else: 233 | return True 234 | 235 | def check_exists(self, file_name): 236 | """Checks if a specified file exists within an extension. 237 | 238 | @param file_name string The file name from a config file 239 | """ 240 | if not self.extension_path: 241 | self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) 242 | return True 243 | ext_zip = zipfile.ZipFile(self.extension_path, 'r') 244 | files = ext_zip.namelist() 245 | if not str(file_name) in files: 246 | self.log.warning(self.translate("logs", "The specified file '{0}' does not exist.".format(file_name))) 247 | return False 248 | else: 249 | return True 250 | 251 | def check_path(self, file_name): 252 | """Runs all path checking functions on a string. 253 | 254 | @param file_name string The string to check for validity. 255 | """ 256 | if not self.check_path_length(file_name): 257 | self.log.warning(self.translate("logs", "This value is too long for your system.")) 258 | return False 259 | if not self.check_path_chars(file_name): 260 | self.log.warning(self.translate("logs", "This value uses invalid characters for your system.")) 261 | return False 262 | return True 263 | 264 | def check_path_chars(self, file_name): 265 | """Checks if a string is a valid file name on this system. 266 | 267 | @param file_name string The string to check for validity 268 | """ 269 | # file length limit 270 | platform = sys.platform 271 | reserved = {"cygwin" : r"[|\?*<\":>+[]/]", 272 | "win32" : r"[|\?*<\":>+[]/]", 273 | "darwin" : "[:]", 274 | "linux" : "[/\x00]"} 275 | if platform and reserved[platform]: 276 | if re.search(file_name, reserved[platform]): 277 | self.log.warning(self.translate("logs", "The extension's config file contains an invalid main value.")) 278 | return False 279 | else: 280 | return True 281 | else: 282 | self.log.warning(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file uses chars that your system does not allow.").format(platform)) 283 | return True 284 | 285 | 286 | def check_path_length(self, file_name=None): 287 | """Checks if a string will be of a valid length for a file name and full path on this system. 288 | 289 | @param file_name string The string to check for validity. 290 | """ 291 | if not self.extension_path: 292 | self.log.debug(self.translate("logs", "No extension directory was specified so file checking was skipped.")) 293 | return True 294 | # file length limit 295 | platform = sys.platform 296 | # OSX(name<=255), linux(name<=255) 297 | name_limit = ['linux', 'darwin'] 298 | # Win(name+path<=260), 299 | path_limit = ['win32', 'cygwin'] 300 | if platform in path_limit: 301 | extension_path = os.path.join(QtCore.QDir.currentPath(), "extensions") 302 | full_path = os.path.join(extension_path, file_name) 303 | if len(str(full_path)) > 255: 304 | self.log.warning(self.translate("logs", "The full extension path cannot be greater than 260 chars")) 305 | return False 306 | else: 307 | return True 308 | elif platform in name_limit: 309 | if len(str(file_name)) >= 260: 310 | self.log.warning(self.translate("logs", "File names can not be greater than 260 chars on your system")) 311 | return False 312 | else: 313 | return True 314 | else: 315 | self.log.warning(self.translate("logs", "Your system, {0} is not recognized. This may cause instability if file or path names are longer than your system allows.").format(platform)) 316 | return True 317 | 318 | class Networking(object): 319 | def __init__(self): 320 | self.log = logging.getLogger("commotion_client."+__name__) 321 | self.translate = QtCore.QCoreApplication.translate 322 | 323 | def ipaddr(self, ip_addr, addr_type=None): 324 | """ 325 | Checks if a string is a validly formatted IPv4 or IPv6 address. 326 | 327 | @param ip str A ip address to be checked 328 | @param addr_type int The appropriate version number: 4 for IPv4, 6 for IPv6. 329 | """ 330 | try: 331 | addr = ipaddress.ip_address(str(ip_addr)) 332 | except ValueError: 333 | self.log.warning(self.translate("logs", "The value {0} is not an validly formed IP-address.").format(ip_addr)) 334 | return False 335 | if addr_type: 336 | if addr.version == addr_type: 337 | return True 338 | else: 339 | self.log.warning(self.translate("logs", "The value {0} is not an validly formed IPv{1}-address.").format(ip_addr, addr_type)) 340 | return False 341 | else: 342 | return True 343 | -------------------------------------------------------------------------------- /commotion_client/commotion_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Commotion_client 6 | 7 | The main python script for implementing the commotion_client GUI. 8 | 9 | Key componenets handled within: 10 | * creation of main GUI 11 | * command line argument parsing 12 | * translation 13 | * initial logging settings 14 | 15 | """ 16 | 17 | import sys 18 | import argparse 19 | import logging 20 | 21 | from PyQt4 import QtGui 22 | from PyQt4 import QtCore 23 | 24 | from commotion_client.utils import logger 25 | from commotion_client.utils import thread 26 | from commotion_client.utils import single_application 27 | from commotion_client.utils import extension_manager 28 | 29 | from commotion_client.GUI import main_window 30 | from commotion_client.GUI import system_tray 31 | 32 | #from controller import CommotionController #TODO Create Controller 33 | 34 | import time 35 | 36 | def get_args(): 37 | #Handle command line arguments 38 | arg_parser = argparse.ArgumentParser(description="Commotion Client") 39 | arg_parser.add_argument("-v", "--verbose", 40 | help="Define the verbosity of the Commotion Client.", 41 | type=int, choices=range(1, 6)) 42 | arg_parser.add_argument("-l", "--logfile", 43 | help="Choose a logfile for this instance") 44 | arg_parser.add_argument("-d", "--daemon", action="store_true", 45 | help="Start the application in Daemon mode (no UI).") 46 | arg_parser.add_argument("-m", "--message", 47 | help="Send a message to any existing Commotion Application") 48 | arg_parser.add_argument("-k", "--key", 49 | help="Choose a unique application key for this Commotion Instance", 50 | type=str) 51 | args = arg_parser.parse_args() 52 | parsed_args = {} 53 | parsed_args['message'] = args.message if args.message else False 54 | #TODO getConfig() #actually want to get this from commotion_config 55 | parsed_args['logLevel'] = args.verbose if args.verbose else 2 56 | parsed_args['logFile'] = args.logfile if args.logfile else None 57 | parsed_args['key'] = ['key'] if args.key else "commotionRocks" #TODO the key is PRIME easter-egg fodder 58 | parsed_args['status'] = "daemon" if args.daemon else False 59 | return parsed_args 60 | 61 | #================================== 62 | # Main Applicaiton Creator 63 | #================================== 64 | 65 | def main(): 66 | """ 67 | Function that handles command line arguments, translation, and creates the main application. 68 | """ 69 | args = get_args() 70 | #Create Instance of Commotion Application 71 | app = CommotionClientApplication(args, sys.argv) 72 | 73 | #Enable Translations #TODO This code needs to be evaluated to ensure that it is pulling in correct translators 74 | locale = QtCore.QLocale.system().name() 75 | qt_translator = QtCore.QTranslator() 76 | if qt_translator.load("qt_"+locale, ":/"): 77 | app.installTranslator(qt_translator) 78 | app_translator = QtCore.QTranslator() 79 | if app_translator.load("imagechanger_"+locale, ":/"): #TODO This code needs to be evaluated to ensure that it syncs with any internationalized images 80 | app.installTranslator(app_translator) 81 | 82 | #check for existing application w/wo a message 83 | if app.is_running(): 84 | if args['message']: 85 | #Checking for custom message 86 | msg = args['message'] 87 | app.send_message(msg) 88 | app.log.info(app.translate("logs", "application is already running, sent following message: \n\"{0}\"".format(msg))) 89 | else: 90 | app.log.info(app.translate("logs", "application is already running. Application will be brought to foreground")) 91 | app.send_message("showMain") 92 | app.end("Only one instance of a commotion application may be running at any time.") 93 | 94 | sys.exit(app.exec_()) 95 | app.log.debug(app.translate("logs", "Shutting down")) 96 | 97 | class HoldStateDuringRestart(thread.GenericThread): 98 | """ 99 | A thread that will run during the restart of all other components to keep the applicaiton alive. 100 | """ 101 | 102 | def __init__(self): 103 | super().__init__() 104 | self.restart_complete = None 105 | self.log = logging.getLogger("commotion_client."+__name__) 106 | 107 | def end(self): 108 | self.restart_complete = True 109 | 110 | def run(self): 111 | self.log.debug(QtCore.QCoreApplication.translate("logs", "Running restart thread")) 112 | while True: 113 | time.sleep(0.3) 114 | if self.restart_complete: 115 | self.log.debug(QtCore.QCoreApplication.translate("logs", "Restart event identified. Thread quitting")) 116 | break 117 | self.end() 118 | 119 | class CommotionClientApplication(single_application.SingleApplicationWithMessaging): 120 | """ 121 | The final layer of the class onion that is the Commotion client. This class includes functions to enable the sub-processes and modules of the Commotion Client (GUI's and controllers). 122 | """ 123 | 124 | restarted = QtCore.pyqtSignal() 125 | 126 | def __init__(self, args, argv): 127 | super().__init__(args['key'], argv) 128 | status = args['status'] 129 | _logfile = args['logFile'] 130 | _loglevel = args['logLevel'] 131 | self.init_logging(_loglevel, _logfile) 132 | #Set Application and Organization Information 133 | self.setOrganizationName("The Open Technology Institute") 134 | self.setOrganizationDomain("commotionwireless.net") 135 | self.setApplicationName(self.translate("main", "Commotion Client")) #special translation case since we are outside of the main application 136 | self.setWindowIcon(QtGui.QIcon(":logo48.png")) 137 | self.setApplicationVersion("1.0") #TODO Generate this on build 138 | self.translate = QtCore.QCoreApplication.translate 139 | self.status = status 140 | self.controller = False 141 | self.main = False 142 | self.sys_tray = False 143 | 144 | #initialize client (GUI, controller, etc) upon event loop start so that exit/quit works on errors. 145 | QtCore.QTimer.singleShot(0, self.init_client) 146 | 147 | 148 | #================================================= 149 | # CLIENT LOGIC 150 | #================================================= 151 | 152 | def init_client(self): 153 | """ 154 | Start up client using current status to determine run_level. 155 | """ 156 | try: 157 | if not self.status: 158 | self.start_full() 159 | elif self.status == "daemon": 160 | self.start_daemon() 161 | except Exception as _excp: #log failure here and exit 162 | _catch_all = self.translate("logs", "Could not fully initialize applicaiton. Application must be halted.") 163 | self.log.critical(_catch_all) 164 | self.log.exception(_excp) 165 | self.end(_catch_all) 166 | 167 | def init_logging(self, level=None, logfile=None): 168 | self.logger = logger.LogHandler("commotion_client", level, logfile) 169 | self.log = self.logger.get_logger() 170 | 171 | def start_full(self): 172 | """ 173 | Start or switch client over to full client. 174 | """ 175 | extensions = extension_manager.ExtensionManager() 176 | if not extensions.check_installed(): 177 | extensions.init_extension_libraries() 178 | if not self.main: 179 | try: 180 | self.main = self.create_main_window() 181 | except Exception as _excp: 182 | _catch_all = self.translate("logs", "Could not create Main Window. Application must be halted.") 183 | self.log.critical(_catch_all) 184 | self.log.exception(_excp) 185 | self.end(_catch_all) 186 | else: 187 | self.init_main() 188 | try: 189 | self.sys_tray = self.create_sys_tray() 190 | except Exception as _excp: 191 | _catch_all = self.translate("logs", "Could not create system tray. Application must be halted.") 192 | self.log.critical(_catch_all) 193 | self.log.exception(_excp) 194 | self.end(_catch_all) 195 | else: 196 | self.init_sys_tray() 197 | 198 | def start_daemon(self): 199 | """ 200 | Start or switch client over to daemon mode. Daemon mode runs the taskbar without showing the main window. 201 | """ 202 | try: 203 | #Close main window without closing taskbar and system tray 204 | if self.main: 205 | self.hide_main_window(force=True, errors="strict") 206 | except Exception as _excp: 207 | self.log.critical(self.translate("logs", "Could not close down existing GUI componenets to switch to daemon mode.")) 208 | self.log.exception(_excp) 209 | raise 210 | try: 211 | #create controller and sys tray 212 | self.sys_tray = self.create_sys_tray() 213 | #if not self.controller: #TODO Actually create a stub controller file 214 | # self.controller = create_controller() 215 | except Exception as _excp: 216 | self.log.critical(self.translate("logs", "Could not start daemon. Application must be halted.")) 217 | self.log.exception(_excp) 218 | raise 219 | else: 220 | self.init_sys_tray() 221 | 222 | def stop_client(self, force_close=None): 223 | """ 224 | Stops all running client processes. 225 | 226 | @param force_close bool Whole application exit if clean close fails. See: close_controller() & close_main_window() 227 | """ 228 | try: 229 | self.close_main_window(force_close) 230 | self.close_sys_tray(force_close) 231 | self.close_controller(force_close) 232 | except Exception as _excp: 233 | if force_close: 234 | _catch_all = self.translate("logs", "Could not cleanly close client. Application must be halted.") 235 | self.log.critical(_catch_all) 236 | self.log.exception(_excp) 237 | self.end(_catch_all) 238 | else: 239 | self.log.error(self.translate("logs", "Client could not be closed.")) 240 | self.log.info(self.translate("logs", "It is reccomended that you restart the application.")) 241 | self.log.exception(_excp) 242 | 243 | def restart_client(self, force_close=None): 244 | """ 245 | Restarts the entire client stack according to current application status. 246 | 247 | @param force_close bool Whole application exit if clean close fails. See: close_controller() & close_main_window() 248 | """ 249 | #hold applicaiton state while restarting all other components. 250 | _restart = HoldStateDuringRestart() 251 | _restart.start() 252 | try: 253 | self.stop_client(force_close) 254 | self.init_client() 255 | except Exception as _excp: 256 | if force_close: 257 | _catch_all = self.translate("logs", "Client could not be restarted. Applicaiton will now be halted") 258 | self.log.error(_catch_all) 259 | self.log.exception(_excp) 260 | self.end(_catch_all) 261 | else: 262 | self.log.error(self.translate("logs", "Client could not be restarted.")) 263 | self.log.info(self.translate("logs", "It is reccomended that you restart the application.")) 264 | self.log.exception(_excp) 265 | raise 266 | _restart.end() 267 | 268 | #================================================= 269 | # MAIN WINDOW 270 | #================================================= 271 | 272 | def create_main_window(self): 273 | """ 274 | Will create a new main window or return existing main window if one is already created. 275 | """ 276 | if self.main: 277 | self.log.debug(self.translate("logs", "New window requested when one already exists. Returning existing main window.")) 278 | self.log.info(self.translate("logs", "If you would like to close the main window and re-open it please call close_main_window() first.")) 279 | return self.main 280 | try: 281 | _main = main_window.MainWindow() 282 | except Exception as _excp: 283 | self.log.critical(self.translate("logs", "Could not create Main Window. Application must be halted.")) 284 | raise 285 | else: 286 | return _main 287 | 288 | def init_main(self): 289 | """ 290 | Main window initializer that shows and connects the main window's messaging function to the app message processor. 291 | """ 292 | try: 293 | self.main.app_message.connect(self.process_message) 294 | if self.sys_tray: 295 | self.sys_tray.exit.triggered.connect(self.main.exitEvent) 296 | self.sys_tray.show_main.connect(self.main.bring_front) 297 | except Exception as _excp: 298 | self.log.error(self.translate("logs", "Could not initialize connections between the main window and other application components.")) 299 | self.log.exception(_excp) 300 | raise 301 | try: 302 | self.main.show() 303 | except Exception as _excp: 304 | self.log.error(self.translate("logs", "Could not show the main window.")) 305 | self.log.exception(_excp) 306 | raise 307 | 308 | def hide_main_window(self, force=None, errors=None): 309 | """ 310 | Attempts to hide the main window without closing the task-bar. 311 | 312 | @param force bool Force window reset if hiding is unsuccessful. 313 | @param errors If set to "strict" errors found will be raised before returning the boolean result. 314 | @return bool Return True if successful and false is unsuccessful. 315 | """ 316 | try: 317 | self.main.exit() 318 | except Exception as _excp: 319 | self.log.error(self.translate("logs", "Could not hide main window. Attempting to close all and only open taskbar.")) 320 | self.log.exception(_excp) 321 | if force: 322 | try: 323 | self.main.purge() 324 | self.main = None 325 | self.main = self.create_main_window() 326 | except Exception as _excp: 327 | self.log.error(self.translate("logs", "Could not force main window restart.")) 328 | self.log.exception(_excp) 329 | raise 330 | elif errors == "strict": 331 | raise 332 | else: 333 | return False 334 | else: 335 | return True 336 | #force hide settings 337 | try: 338 | #if already open, then close first 339 | if self.main: 340 | self.close_main_window() 341 | #re-open 342 | self.main = main_window.MainWindow() 343 | self.main.app_message.connect(self.process_message) 344 | except: 345 | self.log.error(self.translate("logs", "Could close and re-open the main window.")) 346 | self.log.exception(_excp) 347 | if errors == "strict": 348 | raise 349 | else: 350 | return False 351 | else: 352 | return True 353 | return False 354 | 355 | def close_main_window(self, force_close=None): 356 | """ 357 | Closes the main window and task-bar. Only removes the GUI components without closing the application. 358 | 359 | @param force_close bool If the application fails to kill the main window, the whole application should be shut down. 360 | @return bool 361 | """ 362 | try: 363 | self.main.purge 364 | self.main = False 365 | except Exception as _excp: 366 | self.log.error(self.translate("logs", "Could not close main window.")) 367 | if force_close: 368 | self.log.info(self.translate("logs", "force_close activated. Closing application.")) 369 | try: 370 | self.main.deleteLater() 371 | self.main = False 372 | except Exception as _excp: 373 | _catch_all = self.translate("logs", "Could not close main window using its internal mechanisms. Application will be halted.") 374 | self.log.critical(_catch_all) 375 | self.log.exception(_excp) 376 | self.end(_catch_all) 377 | else: 378 | self.log.error(self.translate("logs", "Could not close main window.")) 379 | self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) 380 | self.log.exception(_excp) 381 | raise 382 | 383 | #================================================= 384 | # CONTROLLER 385 | #================================================= 386 | 387 | def create_controller(self): 388 | """ 389 | Creates a controller to act as the middleware between the GUI and the commotion core. 390 | """ 391 | try: 392 | pass #replace when controller is ready 393 | #self.controller = CommotionController() #TODO Implement controller 394 | #self.controller.init() #?????? 395 | except Exception as _excp: 396 | self.log.critical(self.translate("logs", "Could not create controller. Application must be halted.")) 397 | self.log.exception(_excp) 398 | raise 399 | 400 | def init_controller(self): 401 | pass 402 | 403 | def close_controller(self, force_close=None): 404 | """ 405 | Closes the controller process. 406 | 407 | @param force_close bool If the application fails to kill the controller, the whole application should be shut down. 408 | """ 409 | try: 410 | pass #TODO Swap with below when controller close function is instantiated 411 | #if self.controller.close(): 412 | # self.controller = None 413 | except Exception as _excp: 414 | self.log.error(self.translate("logs", "Could not close controller.")) 415 | if force_close: 416 | self.log.info(self.translate("logs", "force_close activated. Closing application.")) 417 | try: 418 | del self.controller 419 | except Exception as _excp: 420 | _catch_all = self.translate("logs", "Could not close controller using its internal mechanisms. Application will be halted.") 421 | self.log.critical(_catch_all) 422 | self.log.exception(_excp) 423 | self.end(_catch_all) 424 | else: 425 | self.log.error(self.translate("logs", "Could not cleanly close controller.")) 426 | self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) 427 | self.log.exception(_excp) 428 | raise 429 | 430 | 431 | #================================================= 432 | # SYSTEM TRAY 433 | #================================================= 434 | 435 | def init_sys_tray(self): 436 | """ 437 | System Tray initializer that runs all processes required to connect the system tray to other application commponents 438 | """ 439 | try: 440 | if self.main: 441 | self.sys_tray.exit.triggered.connect(self.main.exitEvent) 442 | self.sys_tray.show_main.connect(self.main.bring_front) 443 | except Exception as _excp: 444 | self.log.error(self.translate("logs", "Could not initialize connections between the system tray and other application components.")) 445 | self.log.exception(_excp) 446 | raise 447 | 448 | def create_sys_tray(self): 449 | """ 450 | Starts the system tray 451 | """ 452 | try: 453 | tray = system_tray.TrayIcon() 454 | except Exception as _excp: 455 | self.log.error(self.translate("logs", "Could not start system tray.")) 456 | self.log.exception(_excp) 457 | raise 458 | else: 459 | return tray 460 | 461 | def close_sys_tray(self, force_close=None): 462 | """ 463 | Closes the system tray. Only removes the GUI components without closing the application. 464 | 465 | @param force_close bool If the application fails to kill the main window, the whole application should be shut down. 466 | @return bool 467 | """ 468 | try: 469 | self.sys_tray.close() 470 | self.sys_tray = False 471 | except Exception as _excp: 472 | self.log.error(self.translate("logs", "Could not close system tray.")) 473 | if force_close: 474 | self.log.info(self.translate("logs", "force_close activated. Closing application.")) 475 | try: 476 | self.sys_tray.deleteLater() 477 | self.sys_tray.close() 478 | except: 479 | _catch_all = self.translate("logs", "Could not close system tray using its internal mechanisms. Application will be halted.") 480 | self.log.critical(_catch_all) 481 | self.log.exception(_excp) 482 | self.end(_catch_all) 483 | else: 484 | self.log.error(self.translate("logs", "Could not close system tray.")) 485 | self.log.info(self.translate("logs", "It is reccomended that you close the entire application.")) 486 | self.log.exception(_excp) 487 | raise 488 | 489 | #================================================= 490 | # APPLICATION UTILS 491 | #================================================= 492 | 493 | def process_message(self, message): 494 | """ 495 | Process which processes messages an app receives and takes actions on valid requests. 496 | """ 497 | if message == "showMain": 498 | if self.main != False: 499 | self.main.show() 500 | self.main.raise_() 501 | elif message == "restart": 502 | self.log.info(self.translate("logs", "Received a message to restart. Restarting Now.")) 503 | self.restart_client(force_close=True) #TODO, might not want strict here post-development 504 | elif message == "debug": 505 | self.logger.set_verbosity("DEBUG") 506 | else: 507 | self.log.info(self.translate("logs", "message \"{0}\" not a supported type.".format(message))) 508 | 509 | def end(self, message=None): 510 | """ 511 | Handles properly exiting the application. 512 | 513 | @param message string optional exit message to print to standard error on application close. This will FORCE the application to close in an unclean way. 514 | """ 515 | if message: 516 | self.log.error(self.translate("logs", message)) 517 | self.exit(1) 518 | else: 519 | self.quit() 520 | 521 | if __name__ == "__main__": 522 | main() 523 | --------------------------------------------------------------------------------