source: orange/Orange/OrangeCanvas/registry/qt.py @ 11717:b050c4aa5e58

Revision 11717:b050c4aa5e58, 11.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Prettify builtin type names in widget tool tips.

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