# this project is licensed under the WTFPLv2, see COPYING.txt for details
"""Action intents
Intents are generic actions triggered by plugins that are delegated to user configuration and other plugins.
An example is the "`open_editor`" intent. It be triggered by an "Open" dialog (:any:`eye.widgets.filechooser`) when a
file is selected by the user or by a :any:`eye.widgets.locationlist.LocationList` when a location is clicked.
Configuration scripts can register multiple intent callbacks for this intent type. They will be called in turn, until
one of the callbacks handles the intent, for example by opening a new tab (see :any:`eye.helpers.buffers`).
When the intent has been handled, callback processing for this intent stops.
Internally, intents are events and intent listeners are event filters.
"""
from functools import wraps
import logging
from PyQt5.QtCore import QCoreApplication, QEvent, QObject
from eye.connector import register_event_filter, CategoryMixin
from eye.structs import PropDict
__all__ = (
'IntentEvent', 'register_intent_listener', 'dummy_listener', 'send_intent',
'default_open_editor',
)
LOGGER = logging.getLogger(__name__)
[docs]
class IntentEvent(QEvent):
"""Intent
"""
Type = QEvent.registerEventType()
def __init__(self, intent_type, **kwargs):
super().__init__(self.Type)
self.intent_type = intent_type
"""Type of the intent
An intent has a type, which is a simple string, for example `"open_editor"` for the intent to open an
editor with a particular file displayed.
:type: str
"""
self.source = kwargs.pop('source', None)
"""Object that sent the intent
:type: QObject
"""
self.info = PropDict(**kwargs)
"""Extra info about the intent
This info is filled when the intent is sent (:any:`send_intent`).
:type: :any:`eye.structs.PropDict`
"""
self.result = None
"""Result of the intent (if applicable)
Optionally set by a handler with :any:`accept`.
"""
self.ignore()
[docs]
def accept(self, result=None):
"""Accept the intent
When an intent listener handles an `IntentEvent`, it should return True to avoid the intent being
handled multiple times.
Optionally, when the intent is handled, it can also be marked as accepted, with a result value. This
result can then be retrieved by the object which sent the intent. The result is set in the
:any:`result` attribute of the IntentEvent.
For example, there could be an intent for querying user input, an object needing input would send the
intent and would get back the input that one of the listeners produced, in the :any:`result`
attribute.
Or there could be an intent for creating a new tab, and a handler would create a different widget than
the normal :any:`eye.widgets.editor.Editor`, and return it through this result.
"""
self.setAccepted(True)
self.result = result
def __repr__(self):
return '<IntentEvent type=%r source=%r info=%r>' % (self.intent_type, self.source, self.info)
[docs]
def register_intent_listener(intent_type, categories=None, stackoffset=0):
"""Decorate a callback to be registered as an intent listener
See :any:`dummy_listener` for documentation of how the callback should be.
:param intent_type: the type of the intent to listen to
:type intent_type: str
:param categories: If None, listen to intents from any object. Else, listen to intent emitted by objects
matching the categories.
"""
if categories is None:
categories = []
def decorator(cb):
@register_event_filter(categories, [IntentEvent.Type], stackoffset=(1 + stackoffset))
@wraps(cb)
def wrapper(obj, ev):
if getattr(cb, 'enabled', True) and ev.intent_type == intent_type:
res = cb(obj, ev)
if res and not ev.isAccepted():
ev.accept(res)
return bool(res)
return False
return cb
return decorator
[docs]
def send_intent(source, intent_type, **kwargs):
"""Send an intent
If the intent was handled by a listener, it can have a result, see :any:`IntentEvent.result`.
:param source: object sending the intent
:param intent_type: type of the intent
:type intent_type: str
:param kwargs: extra info about the intent
:returns: the result of the intent, if any
"""
LOGGER.debug('sending intent %r with source %r: %r', intent_type, source, kwargs)
if source is None:
source = DefaultSender()
event = IntentEvent(intent_type, source=source, **kwargs)
QCoreApplication.sendEvent(source, event)
if not event.isAccepted():
LOGGER.info("intent %r for %r was not accepted (%r)", intent_type, source, kwargs)
return event.result
[docs]
def dummy_listener(source, intent):
"""Sample intent listener
Intent listeners (see :any:`register_intent_listener`) should follow this function prototype.
The function can handle `intent` and perform appropriate action if desired.
If the function handled the intent, it should return True to mark the intent has having been already processed.
Consequently, no more callbacks are called for this intent, to avoid it being handled multiple times.
If the function handled the intent and returned a "truthy" value, but it did not call :any:`Intent.accept`,
the Intent is automatically accepted and the value returned by the function is considered to be the `Intent`
result (:any:`Intent.result`).
If the function didn't handle the intent, it should return False, so other callbacks have a chance
to handle it.
:param source: object which sent the intent
:type source: QObject
:param intent:
:type intent: IntentEvent
:returns: True if the listener handled the intent, else False
:rtype: bool
"""
return False
[docs]
@register_intent_listener('open_editor')
def default_open_editor(source, ev):
from eye.helpers.buffers import open_editor
editor = open_editor(ev.info.path, ev.info.get('loc'))
return editor
class DefaultSender(QObject, CategoryMixin):
def __init__(self):
super().__init__()
self.add_category('default_sender')