Ejemplo n.º 1
0
class AbstractWebView(QWebEngineView):
    contentLoaded = False
    loadChanged = Signal(usertypes.LoadEvent)

    def __init__(self, parent):
        super().__init__(parent)
        self.loadStarted.connect(self._loadStarted)
        self.loadFinished.connect(self._loadFinished)

    def _loadStarted(self):
        self.contentLoaded = False
        self.loadChanged.emit(usertypes.LoadEvent.STARTED)

    def _loadFinished(self):
        self.contentLoaded = True
        self.loadChanged.emit(usertypes.LoadEvent.FINISHED)

    def load(self, url):
        self.setUrl(url)

    def setUrl(self, url):
        self.loadChanged.emit(usertypes.LoadEvent.BEFORE_LOAD)
        return super().setUrl(url)

    def loadProgress(self, progress):
        return super().loadProgress(progress)
Ejemplo n.º 2
0
class LWidget(QWidget):
    onClose = Signal()
    onDestroyed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        self.destroyed.connect(LWidget._onDestroy)

    @classmethod
    def _onDestroy(cls):
        cls.onDestroyed.emit()

    def closeEvent(self, e):
        self.onClose.emit(e)
        super().closeEvent(e)
Ejemplo n.º 3
0
class Controller(object):
    mainWindowChanged = Signal()
    app = None

    defaultUrl = None
    _mainWindow: Window = None

    @classmethod
    def setMainWindow(cls, window) -> None:
        cls._mainWindow = window
        cls.mainWindowChanged.emit(window)

    @classmethod
    def setDefaultUrl(cls, url: str) -> None:
        cls.defaultUrl = url

    def _onMainWindowChanged(self, window: Window):
        if self.defaultUrl is not None:
            window.loadUrl(self.defaultUrl)

        window.restoreState("main")
        window.onClose.connect(self._onMainWindowClosed)

    def _onMainWindowClosed(self):
        self._mainWindow.saveState("main")

    def _onWindowAdded(self, window: Window):
        if self._mainWindow is None and isinstance(window, Window):
            self.setMainWindow(window)

    def __init__(self, app):
        super().__init__()
        log.controller.debug("Initializing controller")
        self.app = app

        # listen for window added signal in case user is late to register their window
        app.windowAdded.connect(self._onWindowAdded)
        self.mainWindowChanged.connect(self._onMainWindowChanged)

        w: list = app.windows

        if self._mainWindow is not None and len(w) > 0:
            self.setMainWindow(w[0])
        elif self._mainWindow is not None:
            self._mainWindow.restoreState("main")
            if getattr(self, "defaultUrl") is not None:
                self._mainWindow.loadUrl(self.defaultUrl)
Ejemplo n.º 4
0
class AbstractWebPage(QWebEnginePage):
    shuttingDown = Signal()
    _isShuttingDown = False

    def __init__(self, profile, parent):
        super().__init__(profile, parent)

    def shutdown(self):
        self._isShuttingDown = True
        self.shuttingDown.emit()

    def javaScriptAlert(self, securityOrigin, js_msg):
        """Override javaScriptAlert to use luminos prompts."""
        if self._isShuttingDown:
            return
        escape_msg = qtutils.version_check('5.11', compiled=False)
        try:
            Shared.javascript_alert(securityOrigin, js_msg,
                                    abort_on=[self.loadStarted,
                                              self.shuttingDown],
                                    escape_msg=escape_msg)
        except Shared.CallSuper:
            super().javaScriptAlert(securityOrigin, js_msg)

    def javaScriptConsoleMessage(self, level, message, lineNumber, source):
        level_map = {
            QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info,
            QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning,
            QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error,
        }
        Shared.javascript_log_message(level_map[level], source, lineNumber, message)

    def javaScriptPrompt(self, url, js_msg, defaultValue):
        """Override javaScriptPrompt to use luminos prompts."""
        escape_msg = qtutils.version_check('5.11', compiled=False)
        if self._isShuttingDown:
            return (False, "")
        try:
            return Shared.javascript_prompt(url, js_msg, defaultValue,
                                            abort_on=[self.loadStarted,
                                                      self.shuttingDown],
                                            escape_msg=escape_msg)
        except Shared.CallSuper:
            return super().javaScriptPrompt(url, js_msg, defaultValue)
Ejemplo n.º 5
0
class LWebView(AbstractWebView):
    toggleDevTools = Signal()

    def __init__(self, *, private, parent=None):
        super().__init__(parent)
        theme_color = self.style().standardPalette().color(QPalette.Base)
        page = LWebEnginePage(theme_color=theme_color, profile=None, parent=self)
        self.setPage(page)

    def contextMenuEvent(self, e):
        menu = QMenu(self)
        reloadAction = menu.addAction("Reload")
        openDevToolsAction = menu.addAction("Open developer tools")
        action = menu.exec_(self.mapToGlobal(e.pos()))
        if action == openDevToolsAction:
            self.toggleDevTools.emit()
        elif action == reloadAction:
            self.reload()

    def shutdown(self):
        self.page().shutdown()

        # def createWindow(self, wintype):
        """Called by Qt when a page wants to create a new window.
Ejemplo n.º 6
0
class Device:
    servicesResolved = Signal()
    connected = Signal()
    disconnected = Signal()

    # characteristicChanged = Signal(typing.Any, typing.Any)

    def __init__(self, mac_address, manager, managed=True, *args, **kwargs):
        """
        Represents a BLE GATT device.

        This class is intended to be sublcassed with a device-specific implementations
        that reflect the device's GATT profile.

        :param mac_address: MAC address of this device
        :manager: `DeviceManager` that shall manage this device
        :managed: If False, the created device will not be managed by the device manager
                  Particularly of interest for sub classes of `DeviceManager` who want
                  to decide on certain device properties if they then create a subclass
                  instance of that `Device` or not.
        """
        super().__init__(*args, **kwargs)
        self.mac_address = mac_address
        self.manager = manager
        self.services = []

        self._bus = manager._bus
        self._object_manager = manager._object_manager

        # TODO: Device needs to be created if it's not yet known to bluetoothd, see "test-device" in bluez-5.43/test/
        self._device_path = '/org/bluez/%s/dev_%s' % (
            manager.adapter_name, mac_address.replace(':', '_').upper())
        device_object = self._bus.get_object('org.bluez', self._device_path)
        self._object = dbus.Interface(device_object, 'org.bluez.Device1')
        self._properties = dbus.Interface(self._object,
                                          'org.freedesktop.DBus.Properties')
        self._properties_signal = None
        self._connect_retry_attempt = None

        if managed:
            manager._manage_device(self)

    def advertised(self):
        """
        Called when an advertisement package has been received from the device. Requires device discovery to run.
        """
        pass

    def is_registered(self):
        # TODO: Implement, see __init__
        return False

    def register(self):
        # TODO: Implement, see __init__
        return

    def invalidate(self):
        self._disconnect_signals()

    def connect(self):
        """
        Connects to the device. Blocks until the connection was successful.
        """
        self._connect_retry_attempt = 0
        self._connect_signals()
        self._connect()

    def _connect(self):
        self._connect_retry_attempt += 1
        try:
            self._object.Connect()
            if not self.services and self.is_services_resolved():
                self.services_resolved()

        except dbus.exceptions.DBusException as e:
            if (e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownObject'
                ):
                self.connect_failed(
                    Failed(
                        "Device does not exist, check adapter name and MAC address."
                    ))
            elif ((e.get_dbus_name() == 'org.bluez.Error.Failed') and
                  (e.get_dbus_message() == "Operation already in progress")):
                pass
            elif (
                (self._connect_retry_attempt < 5)
                    and (e.get_dbus_name() == 'org.bluez.Error.Failed') and
                (e.get_dbus_message() == "Software caused connection abort")):
                self._connect()
            elif (e.get_dbus_name() == 'org.freedesktop.DBus.Error.NoReply'):
                # TODO: How to handle properly?
                # Reproducable when we repeatedly shut off Nuimo immediately after its flashing Bluetooth icon appears
                self.connect_failed(_error_from_dbus_error(e))
            else:
                self.connect_failed(_error_from_dbus_error(e))

    def _connect_signals(self):
        if self._properties_signal is None:
            self._properties_signal = self._properties.connect_to_signal(
                'PropertiesChanged', self.properties_changed)
        self._connect_service_signals()

    def _connect_service_signals(self):
        for service in self.services:
            service._connect_signals()

    def connect_succeeded(self):
        """
        Will be called when `connect()` has finished connecting to the device.
        Will not be called if the device was already connected.
        """
        pass

    def connect_failed(self, error):
        """
        Called when the connection could not be established.
        """
        self._disconnect_signals()

    def disconnect(self):
        """
        Disconnects from the device, if connected.
        """
        self._object.Disconnect()

    def disconnect_succeeded(self):
        """
        Will be called when the device has disconnected.
        """
        self._disconnect_signals()
        self.services = []

    def _disconnect_signals(self):
        if self._properties_signal is not None:
            self._properties_signal.remove()
            self._properties_signal = None
        self._disconnect_service_signals()

    def _disconnect_service_signals(self):
        for service in self.services:
            service._disconnect_signals()

    def is_connected(self):
        """
        Returns `True` if the device is connected, otherwise `False`.
        """
        return self._properties.Get('org.bluez.Device1', 'Connected') == 1

    def is_services_resolved(self):
        """
        Returns `True` is services are discovered, otherwise `False`.
        """
        return self._properties.Get('org.bluez.Device1',
                                    'ServicesResolved') == 1

    def alias(self):
        """
        Returns the device's alias (name).
        """
        try:
            return self._properties.Get('org.bluez.Device1', 'Alias')
        except dbus.exceptions.DBusException as e:
            if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownObject':
                # BlueZ sometimes doesn't provide an alias, we then simply return `None`.
                # Might occur when device was deleted as the following issue points out:
                # https://github.com/blueman-project/blueman/issues/460
                return None
            else:
                raise _error_from_dbus_error(e)

    def properties_changed(self, sender, changed_properties,
                           invalidated_properties):
        """
        Called when a device property has changed or got invalidated.
        """
        if 'Connected' in changed_properties:
            if changed_properties['Connected']:
                self.connect_succeeded()
                self.connected.emit()
            else:
                self.disconnect_succeeded()
                self.disconnected.emit()

        if ('ServicesResolved' in changed_properties
                and changed_properties['ServicesResolved'] == 1
                and not self.services):
            self.services_resolved()

    def services_resolved(self):
        """
        Called when all device's services and characteristics got resolved.
        """
        self._disconnect_service_signals()

        services_regex = re.compile(self._device_path +
                                    '/service[0-9abcdef]{4}$')
        managed_services = [
            service
            for service in self._object_manager.GetManagedObjects().items()
            if services_regex.match(service[0])
        ]
        self.services = [
            Service(device=self,
                    path=service[0],
                    uuid=service[1]['org.bluez.GattService1']['UUID'])
            for service in managed_services
        ]

        self._connect_service_signals()
        self.servicesResolved.emit()

    def characteristic_value_updated(self, characteristic, value):
        """
        Called when a characteristic value has changed.
        """
        # self.characteristicChanged.emit(characteristic, value)
        # To be implemented by subclass
        pass

    def characteristic_read_value_failed(self, characteristic, error):
        """
        Called when a characteristic value read command failed.
        """
        # To be implemented by subclass
        pass

    def characteristic_write_value_succeeded(self, characteristic):
        """
        Called when a characteristic value write command succeeded.
        """
        # To be implemented by subclass
        pass

    def characteristic_write_value_failed(self, characteristic, error):
        """
        Called when a characteristic value write command failed.
        """
        # To be implemented by subclass
        pass

    def characteristic_enable_notifications_succeeded(self, characteristic):
        """
        Called when a characteristic notifications enable command succeeded.
        """
        # To be implemented by subclass
        pass

    def characteristic_enable_notifications_failed(self, characteristic,
                                                   error):
        """
        Called when a characteristic notifications enable command failed.
        """
        # To be implemented by subclass
        pass
Ejemplo n.º 7
0
class DeviceManager(QObject):
    # devices = {}
    aliases = []
    deviceDiscovered = Signal(Device)
    running = False

    def __init__(self, adapter_name, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.listener = None
        self.adapter_name = adapter_name

        self._bus = dbus.SystemBus()
        try:
            adapter_object = self._bus.get_object('org.bluez',
                                                  '/org/bluez/' + adapter_name)
        except dbus.exceptions.DBusException as e:
            raise _error_from_dbus_error(e)
        object_manager_object = self._bus.get_object("org.bluez", "/")
        self._adapter = dbus.Interface(adapter_object, 'org.bluez.Adapter1')
        self._adapter_properties = dbus.Interface(
            self._adapter, 'org.freedesktop.DBus.Properties')
        self._object_manager = dbus.Interface(
            object_manager_object, "org.freedesktop.DBus.ObjectManager")
        self._device_path_regex = re.compile('^/org/bluez/' + adapter_name +
                                             '/dev((_[A-Z0-9]{2}){6})$')
        self._devices = {}
        self._discovered_devices = {}
        self._interface_added_signal = None
        self._properties_changed_signal = None
        self._main_loop = None

        self.update_devices()

    @property
    def is_adapter_powered(self):
        return self._adapter_properties.Get('org.bluez.Adapter1',
                                            'Powered') == 1

    @is_adapter_powered.setter
    def is_adapter_powered(self, powered):
        return self._adapter_properties.Set('org.bluez.Adapter1', 'Powered',
                                            dbus.Boolean(powered))

    def run(self):
        """
        Starts the main loop that is necessary to receive Bluetooth events from the Bluetooth adapter.

        This call blocks until you call `stop()` to stop the main loop.
        """

        if self.running:
            return

        self._interface_added_signal = self._bus.add_signal_receiver(
            self._interfaces_added,
            dbus_interface='org.freedesktop.DBus.ObjectManager',
            signal_name='InterfacesAdded')

        # TODO: Also listen to 'interfaces removed' events?

        self._properties_changed_signal = self._bus.add_signal_receiver(
            self._properties_changed,
            dbus_interface=dbus.PROPERTIES_IFACE,
            signal_name='PropertiesChanged',
            arg0='org.bluez.Device1',
            path_keyword='path')

        def disconnect_signals():
            for device in self._devices.values():
                device.invalidate()
            self._properties_changed_signal.remove()
            self._interface_added_signal.remove()

        self.running = True
        # self._main_loop = GObject.MainLoop()
        # try:
        #     self._main_loop.run()
        #     disconnect_signals()
        # except Exception:
        #     disconnect_signals()
        #     raise

    def stop(self):
        """
        Stops the main loop started with `start()`
        """
        if self.running:
            self.running = False

    def _manage_device(self, device):
        existing_device = self._devices.get(device.mac_address)
        if existing_device is not None:
            existing_device.invalidate()
        self._devices[device.mac_address] = device

    def update_devices(self):
        managed_objects = self._object_manager.GetManagedObjects().items()
        possible_mac_addresses = [
            self._mac_address(path) for path, _ in managed_objects
        ]
        mac_addresses = [m for m in possible_mac_addresses if m is not None]
        new_mac_addresses = [
            m for m in mac_addresses if m not in self._devices
        ]
        for mac_address in new_mac_addresses:
            self.make_device(mac_address)
        # TODO: Remove devices from `_devices` that are no longer managed, i.e. deleted

    def devices(self):
        """
        Returns all known Bluetooth devices.
        """
        self.update_devices()
        return self._devices.values()

    def start_discovery(self, service_uuids=[]):
        """Starts a discovery for BLE devices with given service UUIDs.

        :param service_uuids: Filters the search to only return devices with given UUIDs.
        """

        discovery_filter = {'Transport': 'le'}
        if service_uuids:  # D-Bus doesn't like empty lists, it needs to guess the type
            discovery_filter['UUIDs'] = service_uuids

        try:
            self._adapter.SetDiscoveryFilter(discovery_filter)
            self._adapter.StartDiscovery()
        except dbus.exceptions.DBusException as e:
            if e.get_dbus_name() == 'org.bluez.Error.NotReady':
                raise NotReady(
                    "Bluetooth adapter not ready. "
                    "Set `is_adapter_powered` to `True` or run 'echo \"power on\" | sudo bluetoothctl'."
                )
            if e.get_dbus_name() == 'org.bluez.Error.InProgress':
                # Discovery was already started - ignore exception
                pass
            else:
                raise _error_from_dbus_error(e)

    def stop_discovery(self):
        """
        Stops the discovery started with `start_discovery`
        """
        try:
            self._adapter.StopDiscovery()
        except dbus.exceptions.DBusException as e:
            if (e.get_dbus_name() == 'org.bluez.Error.Failed') and (
                    e.get_dbus_message() == 'No discovery started'):
                pass
            else:
                raise _error_from_dbus_error(e)

    def _interfaces_added(self, path, interfaces):
        self._device_discovered(path, interfaces)

    def _properties_changed(self, interface, changed, invalidated, path):
        # TODO: Handle `changed` and `invalidated` properties and update device
        self._device_discovered(path, [interface])

    def _device_discovered(self, path, interfaces):
        if 'org.bluez.Device1' not in interfaces:
            return
        mac_address = self._mac_address(path)
        if not mac_address:
            return
        device = self._devices.get(mac_address) or self.make_device(
            mac_address)
        if device is not None:
            self.device_discovered(device)

    def device_discovered(self, device):
        device.advertised()
        self.deviceDiscovered.emit(device)

    def _mac_address(self, device_path):
        match = self._device_path_regex.match(device_path)
        if not match:
            return None
        return match.group(1)[1:].replace('_', ':').lower()

    def make_device(self, mac_address):
        """
        Makes and returns a `Device` instance with specified MAC address.

        Override this method to return a specific subclass instance of `Device`.
        Return `None` if the specified device shall not be supported by this class.
        """
        return Device(mac_address=mac_address, manager=self)

    def add_device(self, mac_address):
        """
        Adds a device with given MAC address without discovery.
        """
        # TODO: Implement
        pass

    def remove_device(self, mac_address):
        """
        Removes a device with the given MAC address
        """
        # TODO: Implement
        pass

    def remove_all_devices(self, skip_alias=None):
        self.update_devices()

        keys_to_be_deleted = []
        for key, device in self._devices.items():
            if skip_alias and device.alias() == skip_alias:
                continue
            mac_address = device.mac_address.replace(':', '_').upper()
            path = '/org/bluez/%s/dev_%s' % (self.adapter_name, mac_address)
            self._adapter.RemoveDevice(path)
            keys_to_be_deleted.append(key)

        for key in keys_to_be_deleted:
            del self._devices[key]

        self.update_devices()
Ejemplo n.º 8
0
class AbstractApplication(QApplication):

    windows: typing.List[AbstractWindow] = []
    windowAdded = Signal()
    windowRemoved = Signal()
    beforeRun = Signal()

    def __init__(self, argv, name):
        qt_argv = utils.cleanArgs(argv)
        super().__init__(qt_argv)

        self.aboutToQuit.connect(self._exiting)
        parser = utils.get_argparser()
        args = parser.parse_args(argv)

        log.init_log(args)
        log.init.debug("Log initialized.")

        args.name = name

        if args.version:
            from luminos.utils import version
            print(version.version())
            sys.exit(0)

        log.init.debug("Initializing directories...")
        standarddir.init(args)

        log.init.debug("Initializing application configuration...")
        if config.instance is None:
            self.config = Configuration(
                path.join(standarddir.config(), "{}.yml".format(name)))
        else:
            self.config = config.instance

        from PyQt5.QtWebEngineWidgets import QWebEnginePage
        if (hasattr(args, "enable_inspector") and not hasattr(
                QWebEnginePage, 'setInspectedPage')):  # only Qt < 5.11
            os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(
                utils.random_port())

    def _exiting(self):
        log.config.debug("saving configuration")
        if self.config is not None:
            self.config.save()

    def __init_subclass__(cls, **kwargs):
        if HAS_PYTHON_3_6:
            super().__init_subclass__()

        cls.__pre_init__()

    @classmethod
    def __pre_init__(cls):
        pass

    def exec_(self):
        self.beforeRun.emit()
        return super().exec_()

    def addWindow(self, window: AbstractWindow) -> None:
        self.windows.append(window)
        self.windowAdded.emit(window)

    def removeWindow(self, window: AbstractWindow) -> None:
        self.windows.remove(window)
        self.windowRemoved.emit(window)

    @classmethod
    def running(self) -> bool:
        return QApplication.instance() is not None
Ejemplo n.º 9
0
class PluginManager(QObject):
    pluginAdded = Signal()
    pluginRemoved = Signal()
    pluginActivated = Signal()
    pluginDeactivated = Signal()
    loadStarted = Signal()
    loadFinished = Signal()
    beforeLoad = Signal()
    bridgeInitialize = Signal()

    def __init__(self, plugins_dirs: list = [], parent=None):
        super().__init__(parent)
        log.plugins.debug("Initializing PluginManager")
        lapp = QApplication.instance()

        from luminos.Application import Application
        assert isinstance(lapp, Application)

        self._plugins = {}
        self._injector = PluginInjector()
        self._loadedPlugins = {}
        self._pluginsResources = {}
        self._pluginsDirs = plugins_dirs + standarddir.defaultPluginsDir()
        self.loadStarted.connect(self._loadStarted)
        self.beforeLoad.connect(self._beforeLoad)
        self.loadFinished.connect(self._loadFinished)
        self.bridgeInitialize.connect(self._bridgeInitialize)
        config.instance.changed.connect(self._pluginsStateChanged)
        self._loadPlugins()

    def _bridgeInitialize(self, page):
        for name, resources in self._pluginsResources.items():
            for resource in resources:
                script_name = name + "_" + os.path.basename(resource)

                if resource.endswith(".js"):
                    injectionPoint = QWebEngineScript.DocumentReady
                    page.injectScript(resource, script_name, injectionPoint)
                elif resource.endswith(".css"):
                    injectionPoint = QWebEngineScript.DocumentReady
                    page.injectStylesheet(resource, script_name,
                                          injectionPoint)

    def _beforeLoad(self, channel, page):
        for name, plugin in self._plugins.items():
            if 'beforeLoad' in dir(plugin):
                plugin.beforeLoad(channel, page)
            elif 'before_load' in dir(plugin):
                plugin.before_load(channel, page)

    def _loadStarted(self, page):
        """"""

        for name, plugin in self._plugins.items():
            if 'loadStarted' in dir(plugin):
                plugin.loadStarted(page)
            elif 'load_started' in dir(plugin):
                plugin.load_started(page)

    def _loadFinished(self, page):
        for name, plugin in self._plugins.items():
            if 'loadFinished' in dir(plugin):
                plugin.loadStarted(page)
            elif 'load_finished' in dir(plugin):
                plugin.load_started(page)

    def addPluginPath(self, path: str):
        assert os.path.isabs(path)
        if not path in self._pluginsDirs:
            self._pluginsDirs.append(path)
            self._loadPlugins()

    def _loadPlugin(self, plugin_name):
        if plugin_name in self._loadedPlugins.keys():
            return self._loadedPlugins[plugin_name]

        identities_paths = []
        for directory in self._pluginsDirs:
            identities_paths += utils.findFiles("*.plugin", directory)

        module = None
        for f in identities_paths:
            info = PluginInfo(f)
            name = f
            if info.has_section("plugin") and info.has_option(
                    "plugin", "Name"):
                name = info.get("plugin", "Name")
            else:
                continue

            if name == plugin_name:
                if not info.isValid():
                    log.plugins.debug(
                        f"Plugin identity {name} is not valid, please read documentation "
                        "about how to write plugin.")
                else:
                    parentdir = os.path.dirname(f)
                    module_path = os.path.join(parentdir,
                                               info.get("plugin", "Module"))
                    if (not module_path.endswith(".py")):
                        module_path += ".py"

                    if os.path.exists(module_path):
                        try:
                            module_path = module_path
                            package = "luminos.plugins.{}.{}".format(
                                name, name)
                            spec = importlib.util.spec_from_file_location(
                                package, module_path)
                            module = importlib.util.module_from_spec(spec)
                            spec.loader.exec_module(module)
                            self._loadedPlugins[name] = module
                        except ImportError:
                            log.plugins.error(
                                f"Unable to load plugin module {name}")

                        break
                    else:
                        log.plugins.warning(
                            f"module specified in {name} doesn't exists, it will be ignored."
                        )

        return module

    def _loadPlugins(self):
        """"""
        identities_paths = []
        for directory in self._pluginsDirs:
            identities_paths += utils.findFiles("*.plugin", directory)

        plugins: typing.List[PluginInfo] = []

        for f in identities_paths:
            info = PluginInfo(f)
            name = f
            if info.has_section("plugin") and info.has_option(
                    "plugin", "Name"):
                name = info.get("plugin", "Name")
            else:
                continue

            # if it's already exists it means that user just add a new plugins directory
            if name in self._loadedPlugins.keys():
                continue

            if not info.isValid():
                log.plugins.debug(
                    f"Plugin identity {name} is not valid, please read documentation "
                    "about how to write plugin.")
            else:
                parentdir = os.path.dirname(f)
                module_path = os.path.join(parentdir,
                                           info.get("plugin", "Module"))

                if (not module_path.endswith(".py")):
                    module_path += ".py"

                if os.path.exists(module_path):
                    info.set("plugin", "Path", module_path)
                    plugins.append(info)
                else:
                    log.plugins.warning(
                        f"module specified in {f} doesn't exists, it will be ignored."
                    )

        log.plugins.info(f"{len(plugins)} plugins found.")
        for plugin in plugins:
            try:
                name = plugin.get("plugin", "Name")
                module_name = plugin.get("plugin", "Module").replace(".py", "")
                module_path = plugin.get("plugin", "Path")

                parentdir = os.path.dirname(module_path)
                log.plugins.info(f"creating namespace for plugin {name}")
                # create a fake module for this plugin namespace
                package = f"{name}"
                module = types.ModuleType(package)
                module.__path__ = parentdir
                self._injector.provide(package, module)

                # TODO: add support to import module in subfolder
                # try to load all python file except for the main file and __init__.py
                for f in utils.findFiles("*.py", parentdir):
                    basename = os.path.splitext(os.path.basename(f))[0]
                    if basename == module_name or basename == '__init__':
                        continue

                    tail = f[len(parentdir + '/'):].replace(os.path.sep,
                                                            '.').replace(
                                                                '.py', '')
                    package = f"{name}.{tail}"
                    m_path = f
                    log.plugins.info(
                        f"load external module for plugin {name} with name {module.__name__}"
                    )
                    spec = importlib.util.spec_from_file_location(
                        package, m_path)
                    module = importlib.util.module_from_spec(spec)
                    module.__path__ = m_path
                    self._injector.provide(package, module)

                log.plugins.info(
                    f"importing main plugin module for plugin {name}")
                package = f"{name}.{module_name}"
                spec = importlib.util.spec_from_file_location(
                    package, module_path)
                module = importlib.util.module_from_spec(spec)
                module.__path__ = module_path
                self._injector.provide(package, module)
                spec.loader.exec_module(module)
                self._loadedPlugins[name] = module
                """
                By default plugin will be enabled if there was no plugin configuration.
                """
                cfg = config.instance.get(f"plugins.{name}")
                shouldActivate = True
                if cfg is None:
                    shouldActivate = False
                    cfg = dict()
                    cfg['enabled'] = True
                    config.instance.set(f"plugins.{name}.enabled", True)

                # if this is the first time the plugin is registered code above will trigger _pluginStateChange
                # and activate it, so we don't need to activate it again here
                if cfg['enabled'] and shouldActivate:
                    if 'activate' in dir(module):
                        module.activate()
                        self._plugins[name] = module

                if plugin.has_option("plugin", "Resources"):
                    resources = ast.literal_eval(
                        plugin.get("plugin", "Resources"))
                    base_path = os.path.dirname(module_path)

                    def to_abspath(path: str):
                        if not os.path.isabs(path):
                            return os.path.join(base_path, path)

                        return path

                    resources = list(map(to_abspath, resources))
                    self._pluginsResources[name] = resources

            except ImportError as e:
                name = plugin.get("plugin", "Name")
                log.plugins.error(
                    f"Unable to load plugin module {name} : ${e.msg}")

    @config.change_filter("plugins")
    def _pluginsStateChanged(self, key: str, value):
        """We only interested with the name and the value"""
        res = re.findall("plugins\\.(.*)\\.enabled", key)
        if key.endswith("enabled") and len(res) > 0:
            name = res[0]
            if not value:
                self.disablePlugin(name)
            elif value:
                self.enablePlugin(name)

    def enablePlugin(self, name: str):
        """"""
        log.plugins.debug(f"enabling plugin {name}")
        if not name in self._plugins.keys():
            module = self._loadPlugin(name)
            if module is not None:
                if "activate" in dir(module):
                    module.activate()
                    self.pluginActivated.emit(name)
                    self._plugins[name] = module
                    self.pluginAdded.emit(name)
            else:
                log.plugins.warning(f"Unable activate plugin {name}")

    def disablePlugin(self, name: str):
        """"""
        log.plugins.debug(f"disabling plugin {name}")
        if name in self._plugins.keys():
            module = self._plugins[name]
            if "deactivate" in dir(module):
                module.deactivate()
                self.pluginDeactivated.emit(name)

            self._plugins.pop(name, None)
            self.pluginRemoved.emit(name)
Ejemplo n.º 10
0
class AbstractWindow(QWidget):
    app = None
    config = None
    settings = None
    onClose = Signal()

    def __init__(self, application=None):
        """"""
        log.init.debug("initializing Window")
        super().__init__()

        # self register our window to application
        from luminos.core.AbstractApplication import AbstractApplication

        if application is not None and isinstance(application, AbstractApplication):
            self.app = application
            self.config = application.config
            self.app.addWindow(self)

        self.setWindowFlags(Qt.Window)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        self.setMinimumSize(QSize(640, 480))

    def closeEvent(self, e):
        super().closeEvent(e)
        self.onClose.emit()
        # unregister our window from application
        if self.app is not None:
            self.app.removeWindow(self)

    def __init_subclass__(cls, **kwargs):
        if HAS_PYTHON_3_6:
            super().__init_subclass__()

        cls.__pre_init__()

    @classmethod
    def __pre_init__(cls):
        pass

    def restoreState(self, name: str):
        width = self.config.get("windows.{}.width".format(name))
        height = self.config.get("windows.{}.height".format(name))
        if width is not None and height is not None:
            self.resize(QSize(width, height))

        maximized = self.config.get("windows.{}.maximized".format(name))
        if maximized is not None:
            if maximized:
                self.setWindowState(Qt.WindowMaximized)

        # move window position to the center of the screen
        centerPoint = QDesktopWidget().availableGeometry().center()
        frameGm = self.frameGeometry()
        frameGm.moveCenter(centerPoint)
        self.move(frameGm.topLeft())

    def saveState(self, name: str):
        maximized = self.isMaximized()

        if not maximized:
            self.config.set("windows.{}.width".format(name), self.width())
            self.config.set("windows.{}.height".format(name), self.height())

        self.config.set("windows.{}.maximized".format(name), maximized)

        self.config.save()