1"""
2Qt Model classes for widget registry.
3
4"""
5import bisect
6import warnings
7
8from typing import Union
9
10from xml.sax.saxutils import escape
11from urllib.parse import urlencode
12
13from AnyQt.QtWidgets import QAction
14from AnyQt.QtGui import QStandardItemModel, QStandardItem, QColor, QBrush
15from AnyQt.QtCore import QObject, Qt
16from AnyQt.QtCore import pyqtSignal as Signal
17
18from ..utils import type_str
19from .discovery import WidgetDiscovery
20from .description import WidgetDescription, CategoryDescription
21from .base import WidgetRegistry
22from ..resources import icon_loader
23
24from . import cache, NAMED_COLORS, DEFAULT_COLOR
25
26
27class QtWidgetDiscovery(QObject, WidgetDiscovery):
28    """
29    Qt interface class for widget discovery.
30    """
31    # Discovery has started
32    discovery_start = Signal()
33    # Discovery has finished
34    discovery_finished = Signal()
35    # Processing widget with name
36    discovery_process = Signal(str)
37    # Found a widget with description
38    found_widget = Signal(WidgetDescription)
39    # Found a category with description
40    found_category = Signal(CategoryDescription)
41
42    def __init__(self, parent=None, registry=None, cached_descriptions=None):
43        QObject.__init__(self, parent)
44        WidgetDiscovery.__init__(self, registry, cached_descriptions)
45
46    def run(self, entry_points_iter):
47        self.discovery_start.emit()
48        WidgetDiscovery.run(self, entry_points_iter)
49        self.discovery_finished.emit()
50
51    def handle_widget(self, description):
52        self.discovery_process.emit(description.name)
53        self.found_widget.emit(description)
54
55    def handle_category(self, description):
56        self.found_category.emit(description)
57
58
59class QtWidgetRegistry(QObject, WidgetRegistry):
60    """
61    A QObject wrapper for `WidgetRegistry`
62
63    A QStandardItemModel instance containing the widgets in
64    a tree (of depth 2). The items in a model can be quaries using standard
65    roles (DisplayRole, BackgroundRole, DecorationRole ToolTipRole).
66    They also have QtWidgetRegistry.CATEGORY_DESC_ROLE,
67    QtWidgetRegistry.WIDGET_DESC_ROLE, which store Category/WidgetDescription
68    respectfully. Furthermore QtWidgetRegistry.WIDGET_ACTION_ROLE stores an
69    default QAction which can be used for widget creation action.
70
71    """
72
73    CATEGORY_DESC_ROLE = Qt.ItemDataRole(Qt.UserRole + 1)
74    """Category Description Role"""
75
76    WIDGET_DESC_ROLE = Qt.ItemDataRole(Qt.UserRole + 2)
77    """Widget Description Role"""
78
79    WIDGET_ACTION_ROLE = Qt.ItemDataRole(Qt.UserRole + 3)
80    """Widget Action Role"""
81
82    BACKGROUND_ROLE = Qt.ItemDataRole(Qt.UserRole + 4)
83    """Background color for widget/category in the canvas
84    (different from Qt.BackgroundRole)
85    """
86
87    category_added = Signal(str, CategoryDescription)
88    """signal: category_added(name: str, desc: CategoryDescription)
89    """
90
91    widget_added = Signal(str, str, WidgetDescription)
92    """signal widget_added(category_name: str, widget_name: str,
93                           desc: WidgetDescription)
94    """
95
96    reset = Signal()
97    """signal: reset()
98    """
99
100    def __init__(self, other_or_parent=None, parent=None):
101        if isinstance(other_or_parent, QObject) and parent is None:
102            parent, other_or_parent = other_or_parent, None
103        QObject.__init__(self, parent)
104        WidgetRegistry.__init__(self, other_or_parent)
105
106        # Should  the QStandardItemModel be subclassed?
107        self.__item_model = QStandardItemModel(self)
108
109        for i, desc in enumerate(self.categories()):
110            cat_item = self._cat_desc_to_std_item(desc)
111            self.__item_model.insertRow(i, cat_item)
112
113            for j, wdesc in enumerate(self.widgets(desc.name)):
114                widget_item = self._widget_desc_to_std_item(wdesc, desc)
115                cat_item.insertRow(j, widget_item)
116
117    def model(self):
118        # type: () -> QStandardItemModel
119        """
120        Return the widget descriptions in a Qt Item Model instance
121        (QStandardItemModel).
122
123        .. note:: The model should not be modified outside of the registry.
124
125        """
126        return self.__item_model
127
128    def item_for_widget(self, widget):
129        # type: (Union[str, WidgetDescription]) -> QStandardItem
130        """Return the QStandardItem for the widget.
131        """
132        if isinstance(widget, str):
133            widget = self.widget(widget)
134        cat = self.category(widget.category or "Unspecified")
135        cat_ind = self.categories().index(cat)
136        cat_item = self.model().item(cat_ind)
137        widget_ind = self.widgets(cat).index(widget)
138        return cat_item.child(widget_ind)
139
140    def action_for_widget(self, widget):
141        # type: (Union[str, WidgetDescription]) -> QAction
142        """
143        Return the QAction instance for the widget (can be a string or
144        a WidgetDescription instance).
145
146        """
147        item = self.item_for_widget(widget)
148        return item.data(self.WIDGET_ACTION_ROLE)
149
150    def create_action_for_item(self, item):
151        # type: (QStandardItem) -> QAction
152        """
153        Create a QAction instance for the widget description item.
154        """
155        name = item.text()
156        tooltip = item.toolTip()
157        whatsThis = item.whatsThis()
158        icon = item.icon()
159        action = QAction(
160            icon, name, self, toolTip=tooltip, whatsThis=whatsThis,
161            statusTip=name
162        )
163        widget_desc = item.data(self.WIDGET_DESC_ROLE)
164        action.setData(widget_desc)
165        action.setProperty("item", item)
166        return action
167
168    def _insert_category(self, desc):
169        # type: (CategoryDescription) -> None
170        """
171        Override to update the item model and emit the signals.
172        """
173        priority = desc.priority
174        priorities = [c.priority for c, _ in self.registry]
175        insertion_i = bisect.bisect_right(priorities, priority)
176
177        WidgetRegistry._insert_category(self, desc)
178
179        cat_item = self._cat_desc_to_std_item(desc)
180        self.__item_model.insertRow(insertion_i, cat_item)
181
182        self.category_added.emit(desc.name, desc)
183
184    def _insert_widget(self, category, desc):
185        # type: (CategoryDescription, WidgetDescription) -> None
186        """
187        Override to update the item model and emit the signals.
188        """
189        assert isinstance(category, CategoryDescription)
190        categories = self.categories()
191        cat_i = categories.index(category)
192        _, widgets = self._categories_dict[category.name]
193        priorities = [w.priority for w in widgets]
194        insertion_i = bisect.bisect_right(priorities, desc.priority)
195
196        WidgetRegistry._insert_widget(self, category, desc)
197
198        cat_item = self.__item_model.item(cat_i)
199        widget_item = self._widget_desc_to_std_item(desc, category)
200
201        cat_item.insertRow(insertion_i, widget_item)
202
203        self.widget_added.emit(category.name, desc.name, desc)
204
205    def _cat_desc_to_std_item(self, desc):
206        # type: (CategoryDescription) -> QStandardItem
207        """
208        Create a QStandardItem for the category description.
209        """
210        item = QStandardItem()
211        item.setText(desc.name)
212
213        if desc.icon:
214            icon = desc.icon
215        else:
216            icon = "icons/default-category.svg"
217
218        icon = icon_loader.from_description(desc).get(icon)
219        item.setIcon(icon)
220
221        if desc.background:
222            background = desc.background
223        else:
224            background = DEFAULT_COLOR
225
226        background = NAMED_COLORS.get(background, background)
227
228        brush = QBrush(QColor(background))
229        item.setData(brush, self.BACKGROUND_ROLE)
230
231        tooltip = desc.description if desc.description else desc.name
232
233        item.setToolTip(tooltip)
234        item.setFlags(Qt.ItemIsEnabled)
235        item.setData(desc, self.CATEGORY_DESC_ROLE)
236        return item
237
238    def _widget_desc_to_std_item(self, desc, category):
239        # type: (WidgetDescription, CategoryDescription) -> QStandardItem
240        """
241        Create a QStandardItem for the widget description.
242        """
243        item = QStandardItem(desc.name)
244        item.setText(desc.name)
245
246        if desc.icon:
247            icon = desc.icon
248        else:
249            icon = "icons/default-widget.svg"
250
251        icon = icon_loader.from_description(desc).get(icon)
252        item.setIcon(icon)
253
254        # This should be inherited from the category.
255        background = None
256        if desc.background:
257            background = desc.background
258        elif category.background:
259            background = category.background
260        else:
261            background = DEFAULT_COLOR
262
263        if background is not None:
264            background = NAMED_COLORS.get(background, background)
265            brush = QBrush(QColor(background))
266            item.setData(brush, self.BACKGROUND_ROLE)
267
268        tooltip = tooltip_helper(desc)
269        style = "ul { margin-top: 1px; margin-bottom: 1px; }"
270        tooltip = TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
271        item.setToolTip(tooltip)
272        item.setWhatsThis(whats_this_helper(desc))
273        item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
274        item.setData(desc, self.WIDGET_DESC_ROLE)
275
276        # Create the action for the widget_item
277        action = self.create_action_for_item(item)
278        item.setData(action, self.WIDGET_ACTION_ROLE)
279        return item
280
281
282TOOLTIP_TEMPLATE = """\
283<html>
284<head>
285<style type="text/css">
286{style}
287</style>
288</head>
289<body>
290{tooltip}
291</body>
292</html>
293"""
294
295
296def tooltip_helper(desc):
297    # type: (WidgetDescription) -> str
298    """Widget tooltip construction helper.
299
300    """
301    tooltip = []
302    tooltip.append("<b>{name}</b>".format(name=escape(desc.name)))
303
304    if desc.project_name and desc.project_name != "Orange":
305        tooltip[0] += " (from {0})".format(desc.project_name)
306
307    if desc.description:
308        tooltip.append("{0}".format(
309                            escape(desc.description)))
310
311    inputs_fmt = "<li>{name} ({class_name})</li>"
312
313    if desc.inputs:
314        inputs = "".join(inputs_fmt.format(name=inp.name,
315                                           class_name=type_str(inp.types))
316                         for inp in desc.inputs)
317        tooltip.append("Inputs:<ul>{0}</ul>".format(inputs))
318    else:
319        tooltip.append("No inputs")
320
321    if desc.outputs:
322        outputs = "".join(inputs_fmt.format(name=out.name,
323                                            class_name=type_str(out.types))
324                          for out in desc.outputs)
325        tooltip.append("Outputs:<ul>{0}</ul>".format(outputs))
326    else:
327        tooltip.append("No outputs")
328
329    return "<hr/>".join(tooltip)
330
331
332def whats_this_helper(desc, include_more_link=False):
333    # type: (WidgetDescription, bool) -> str
334    """
335    A `What's this` text construction helper. If `include_more_link` is
336    True then the text will include a `more...` link.
337
338    """
339    title = desc.name
340    help_url = desc.help
341
342    if not help_url:
343        help_url = "help://search?" + urlencode({"id": desc.qualified_name})
344
345    description = desc.description
346    long_description = desc.long_description
347
348    template = ["<h3>{0}</h3>".format(escape(title))]
349
350    if description:
351        template.append("<p>{0}</p>".format(escape(description)))
352
353    if long_description:
354        template.append("<p>{0}</p>".format(escape(long_description[:100])))
355
356    if help_url and include_more_link:
357        template.append("<a href='{0}'>more...</a>".format(escape(help_url)))
358
359    return "\n".join(template)
360
361
362def run_discovery(entry_points_iter, cached=False):
363    warnings.warn(
364        "run_discovery is deprecated and will be removed.",
365        FutureWarning, stacklevel=2
366    )
367    reg_cache = {}
368    if cached:
369        reg_cache = cache.registry_cache()
370
371    discovery = QtWidgetDiscovery(cached_descriptions=reg_cache)
372    registry = QtWidgetRegistry()
373    discovery.found_category.connect(registry.register_category)
374    discovery.found_widget.connect(registry.register_widget)
375    discovery.run()
376    if cached:
377        cache.save_registry_cache(reg_cache)
378    return registry
379