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