"""
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