Source code for orangecanvas.config

"""
Orange Canvas Configuration

"""

import os
import sys
import logging
import warnings
import typing
import pkgutil

from typing import Dict, Optional, Tuple, List, Union, Iterable, Any

import packaging.version

from AnyQt.QtGui import (
    QPainter, QFont, QFontMetrics, QColor, QPixmap, QImage, QIcon
)

from AnyQt.QtCore import (
    Qt, QCoreApplication, QPoint, QRect, QSettings, QStandardPaths, QEvent
)

from .gui.utils import windows_set_current_process_app_user_model_id
from .gui.svgiconengine import SvgIconEngine
from .utils.settings import Settings, config_slot
from .utils.pkgmeta import EntryPoint, Distribution, entry_points

if typing.TYPE_CHECKING:
    import requests
    from .scheme import Scheme
    T = typing.TypeVar("T")


log = logging.getLogger(__name__)

__version__ = "0.0"


#: Entry point by which widgets are registered.
WIDGETS_ENTRY = "orangecanvas.widgets"

#: Entry point by which add-ons register with importlib.metadata
ADDONS_ENTRY = "orangecanvas.addon"

#: Parameters for searching add-on packages in PyPi using xmlrpc api.
ADDON_PYPI_SEARCH_SPEC = {"keywords": ["orange", "add-on"]}

EXAMPLE_WORKFLOWS_ENTRY = "orangecanvas.examples"


def standard_location(type):
    warnings.warn(
        "Use QStandardPaths.writableLocation", DeprecationWarning,
        stacklevel=2
    )
    return QStandardPaths.writableLocation(type)


standard_location.DesktopLocation = QStandardPaths.DesktopLocation      # type: ignore
standard_location.DataLocation = QStandardPaths.AppLocalDataLocation    # type: ignore
standard_location.CacheLocation = QStandardPaths.CacheLocation          # type: ignore
standard_location.DocumentsLocation = QStandardPaths.DocumentsLocation  # type: ignore


class Config:
    """
    Application configuration.
    """
    #: Organization domain
    OrganizationDomain = ""  # type: str
    #: The application name
    ApplicationName = ""     # type: str
    #: Version
    ApplicationVersion = ""  # type: str
    #: AppUserModelID as used on windows for grouping in the task bar
    #: (https://docs.microsoft.com/en-us/windows/win32/shell/appids).
    #: This ensures the program does not group with other Python programs
    #: and gets its own task icon.
    AppUserModelID = None    # type: Optional[str]

[docs] def init(self): """ Initialize the QCoreApplication.organizationDomain, applicationName, applicationVersion and the default settings format. Should only be run once at application startup. """ QCoreApplication.setOrganizationDomain(self.OrganizationDomain) QCoreApplication.setApplicationName(self.ApplicationName) QCoreApplication.setApplicationVersion(self.ApplicationVersion) QSettings.setDefaultFormat(QSettings.IniFormat) app = QCoreApplication.instance() if self.AppUserModelID: windows_set_current_process_app_user_model_id(self.AppUserModelID) if app is not None: QCoreApplication.sendEvent(app, QEvent(QEvent.PolishRequest))
[docs] def application_icon(self): # type: () -> QIcon """ Return the main application icon. """ return QIcon()
[docs] def splash_screen(self): # type: () -> Tuple[QPixmap, QRect] """ Return a splash screen pixmap and an text area within it. The text area is used for displaying text messages during application startup. The default implementation returns a bland rectangle splash screen. Returns ------- t : Tuple[QPixmap, QRect] A QPixmap and a rect area within it. """ return QPixmap(), QRect()
def widgets_entry_points(self): # type: () -> Iterable[EntryPoint] """ Return an iterator over entry points defining the set of 'nodes/widgets' available to the workflow model. """ return iter(()) def addon_entry_points(self): # type: () -> Iterable[EntryPoint] return iter(())
[docs] def addon_pypi_search_spec(self): return {}
def addon_defaults_list( self, session=None # type: Optional[requests.Session] ): # type: (...) -> List[Dict[str, Union[str, list, dict, int, float]]] """ Return a list of default add-ons. The return value must be a list with meta description following the `PyPI JSON api`_ specification. At the minimum 'info.name' and 'info.version' must be supplied. e.g. `[{'info': {'name': 'Super Pkg', 'version': '4.2'}}] .. _`PyPI JSON api`: https://warehouse.readthedocs.io/api-reference/json/ """ return [] def core_packages(self): # type: () -> List[str] """ Return a list of core packages. List of packages that are core of the application. Most importantly, if they themselves define add-on/plugin entry points they must not be 'uninstalled' via a package manager, they can only be updated. Return ------ packages : List[str] A list of package names (can also contain PEP-440 version specifiers). """ return ["orange-canvas-core >= 0.1a, < 0.2a"] def examples_entry_points(self): # type: () -> Iterable[EntryPoint] """ Return an iterator over entry points defining example/preset workflows. """ return iter(()) def widget_discovery(self, *args, **kwargs): raise NotImplementedError def workflow_constructor(self, *args, **kwargs): # type: (Any, Any) -> Scheme """ The default workflow constructor. """ raise NotImplementedError #: Standard application urls. If defined to a valid url appropriate actions #: are defined in various contexts APPLICATION_URLS = { #: Submit a bug report action in the Help menu "Bug Report": None, #: A url quick tour/getting started url "Quick Start": None, #: An url to the full documentation "Documentation": None, #: Video screencast/tutorials "Screencasts": None, #: Used for 'Submit Feedback' action in the help menu "Feedback": None, } # type: Dict[str, Optional[str]] class Default(Config): OrganizationDomain = "biolab.si" ApplicationName = "Orange Canvas Core" ApplicationVersion = __version__ @staticmethod def application_icon(): """ Return the main application icon. """ data = pkgutil.get_data(__name__, "icons/orange-canvas.svg") return QIcon(SvgIconEngine(data)) @staticmethod def splash_screen(): # type: () -> Tuple[QPixmap, QRect] """ Return a splash screen pixmap and an text area within it. The text area is used for displaying text messages during application startup. The default implementation returns a bland rectangle splash screen. Returns ------- t : Tuple[QPixmap, QRect] A QPixmap and a rect area within it. """ contents = pkgutil.get_data(__name__, "icons/orange-canvas-core-splash.svg") img = QImage.fromData(contents, "svg") pm = QPixmap.fromImage(img) version = QCoreApplication.applicationVersion() if version: version_parsed = packaging.version.Version(version) version_comp = version_parsed.release version = ".".join(map(str, version_comp[:2])) size = 21 if len(version) < 5 else 16 font = QFont() font.setPixelSize(size) font.setBold(True) font.setItalic(True) font.setLetterSpacing(QFont.AbsoluteSpacing, 2) metrics = QFontMetrics(font) br = metrics.boundingRect(version).adjusted(-5, 0, 5, 0) br.moveBottomRight(QPoint(pm.width() - 15, pm.height() - 15)) p = QPainter(pm) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.TextAntialiasing) p.setFont(font) p.setPen(QColor("#231F20")) p.drawText(br, Qt.AlignCenter, version) p.end() textarea = QRect(15, 15, 170, 20) return pm, textarea @staticmethod def widgets_entry_points() -> Iterable[EntryPoint]: """ Return an iterator over entry points defining the set of 'nodes/widgets' available to the workflow model. """ return iter(entry_points(group=WIDGETS_ENTRY)) @staticmethod def addon_entry_points() -> Iterable[EntryPoint]: return iter(entry_points(group=ADDONS_ENTRY)) @staticmethod def addon_pypi_search_spec(): return dict(ADDON_PYPI_SEARCH_SPEC) @staticmethod def addon_defaults_list(session=None): """ Return a list of default add-ons. The return value must be a list with meta description following the `PyPI JSON api`_ specification. At the minimum 'info.name' and 'info.version' must be supplied. e.g. `[{'info': {'name': 'Super Pkg', 'version': '4.2'}}] .. _`PyPI JSON api`: https://warehouse.readthedocs.io/api-reference/json/ """ return [] @staticmethod def core_packages(): # type: () -> List[str] """ Return a list of core packages. List of packages that are core of the product. Most importantly, if they themselves define add-on/plugin entry points they must not be 'uninstalled' via a package manager, they can only be updated. Return ------ packages : List[str] A list of package names (can also contain PEP-440 version specifiers). """ return ["orange-canvas-core >= 0.0, < 0.1a"] @staticmethod def examples_entry_points(): return iter(entry_points(group=EXAMPLE_WORKFLOWS_ENTRY)) @staticmethod def widget_discovery(*args, **kwargs): from . import registry return registry.WidgetDiscovery(*args, **kwargs) @staticmethod def workflow_constructor(*args, **kwargs): from . import scheme return scheme.Scheme(*args, **kwargs) default = Default() def init(): """ Initialize the QCoreApplication.organizationDomain, applicationName, applicationVersion and the default settings format. Will only run once. .. note:: This should not be run before QApplication has been initialized. Otherwise it can break Qt's plugin search paths. """ default.init() # Make consecutive calls a null op. global init log.debug("Activating configuration for {}".format(default)) init = lambda: None rc = {} # type: ignore spec = \ [("startup/show-splash-screen", bool, True, "Show splash screen on startup"), ("startup/show-welcome-screen", bool, True, "Show Welcome screen on startup"), ("startup/load-crashed-workflows", bool, True, "Load crashed scratch workflows on startup"), ("stylesheet", str, "orange", "QSS stylesheet to use"), ("schemeinfo/show-at-new-scheme", bool, False, "Show Workflow Properties when creating a new Workflow"), ("mainwindow/scheme-margins-enabled", bool, False, "Show margins around the workflow view"), ("mainwindow/show-scheme-shadow", bool, True, "Show shadow around the workflow view"), ("mainwindow/toolbox-dock-exclusive", bool, False, "Should the toolbox show only one expanded category at the time"), ("mainwindow/toolbox-dock-floatable", bool, False, "Is the canvas toolbox floatable (detachable from the main window)"), ("mainwindow/toolbox-dock-movable", bool, True, "Is the canvas toolbox movable (between left and right edge)"), ("mainwindow/toolbox-dock-use-popover-menu", bool, True, "Use a popover menu to select a widget when clicking on a category " "button"), ("mainwindow/widgets-float-on-top", bool, False, "Float widgets on top of other windows"), ("mainwindow/number-of-recent-schemes", int, 15, "Number of recent workflows to keep in history"), ("schemeedit/show-channel-names", bool, True, "Show channel names"), ("schemeedit/show-link-state", bool, True, "Show link state hints."), ("schemeedit/enable-node-animations", bool, True, "Enable node animations."), ("schemeedit/freeze-on-load", bool, False, "Freeze signal propagation when loading a workflow."), ("quickmenu/trigger-on-double-click", bool, True, "Show quick menu on double click."), ("quickmenu/trigger-on-right-click", bool, True, "Show quick menu on right click."), ("quickmenu/trigger-on-space-key", bool, True, "Show quick menu on space key press."), ("quickmenu/trigger-on-any-key", bool, False, "Show quick menu on double click."), ("quickmenu/show-categories", bool, False, "Show categories in quick menu."), ("logging/level", int, 1, "Logging level"), ("logging/show-on-error", bool, True, "Show log window on error"), ("logging/dockable", bool, True, "Allow log window to be docked"), ("help/open-in-external-browser", bool, False, "Open help in an external browser"), ("add-ons/allow-conda", bool, True, "Install add-ons with conda"), ("add-ons/pip-install-arguments", str, '', 'Arguments to pass to "pip install" when installing add-ons.'), ("network/http-proxy", str, '', 'HTTP proxy.'), ("network/https-proxy", str, '', 'HTTPS proxy.'), ] spec = [config_slot(*t) for t in spec] def register_setting(key, type, default, doc=""): # type: (str, typing.Type[T], T, str) -> None """ Register an application setting. This only affects the `Settings` instance as returned by `settings`. Parameters ---------- key : str The setting key path type : Type[T] Type of the setting. One of `str`, `bool` or `int` default : T Default value for setting. doc : str Setting description string. """ spec.append(config_slot(key, type, default, doc)) def settings(): init() store = QSettings() settings = Settings(defaults=spec, store=store) return settings def data_dir(): """ Return the application data directory. If the directory path does not yet exists then create it. """ init() datadir = QStandardPaths.writableLocation(QStandardPaths.AppLocalDataLocation) version = QCoreApplication.applicationVersion() datadir = os.path.join(datadir, version) if not os.path.isdir(datadir): try: os.makedirs(datadir, exist_ok=True) except OSError: pass return datadir def cache_dir(): """ Return the application cache directory. If the directory path does not yet exists then create it. """ init() cachedir = QStandardPaths.writableLocation(QStandardPaths.CacheLocation) version = QCoreApplication.applicationVersion() cachedir = os.path.join(cachedir, version) if not os.path.exists(cachedir): os.makedirs(cachedir) return cachedir def log_dir(): """ Return the application log directory. """ init() if sys.platform == "darwin": name = str(QCoreApplication.applicationName()) logdir = os.path.join(os.path.expanduser("~/Library/Logs"), name) else: logdir = data_dir() if not os.path.exists(logdir): os.makedirs(logdir) return logdir def widget_settings_dir(): """ Return the widget settings directory. """ warnings.warn( "'widget_settings_dir' is deprecated.", DeprecationWarning, stacklevel=2 ) return os.path.join(data_dir(), 'widgets') def open_config(): warnings.warn( "open_config was never used and will be removed in the future", DeprecationWarning, stacklevel=2 ) return def save_config(): warnings.warn( "save_config was never used and will be removed in the future", DeprecationWarning, stacklevel=2 ) def widgets_entry_points(): """ Return an `EntryPoint` iterator for all 'orange.widget' entry points plus the default Orange Widgets. """ return default.widgets_entry_points() def splash_screen(): """ """ return default.splash_screen() def application_icon(): """ Return the main application icon. """ return default.application_icon() def widget_discovery(*args, **kwargs): return default.widget_discovery(*args, **kwargs) def workflow_constructor(*args, **kwargs): # type: (Any, Any) -> Scheme return default.workflow_constructor(*args, **kwargs) def set_default(conf): global default default = conf