1# ------------------------------------------------------------------------------ 2# Copyright (c) 2008, Riverbank Computing Limited 3# All rights reserved. 4# 5# This software is provided without warranty under the terms of the BSD license. 6# However, when used with the GPL version of PyQt the additional terms 7# described in the PyQt GPL exception also apply 8 9# 10# Author: Riverbank Computing Limited 11# ------------------------------------------------------------------------------ 12 13""" Defines the various instance editors and the instance editor factory for 14 the PyQt user interface toolkit.. 15""" 16 17 18from pyface.qt import QtCore, QtGui 19 20from traits.api import HasTraits, Instance, Property 21 22# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward 23# compatibility. The class has been moved to the 24# traitsui.editors.instance_editor file. 25from traitsui.editors.instance_editor import ToolkitEditorFactory 26from traitsui.ui_traits import AView 27from traitsui.helper import user_name_for 28from traitsui.handler import Handler 29from traitsui.instance_choice import InstanceChoiceItem 30from .editor import Editor 31from .drop_editor import _DropEventFilter 32from .constants import DropColor 33from .helper import position_window 34 35 36OrientationMap = { 37 "default": None, 38 "horizontal": QtGui.QBoxLayout.LeftToRight, 39 "vertical": QtGui.QBoxLayout.TopToBottom, 40} 41 42 43class CustomEditor(Editor): 44 """ Custom style of editor for instances. If selection among instances is 45 allowed, the editor displays a combo box listing instances that can be 46 selected. If the current instance is editable, the editor displays a panel 47 containing trait editors for all the instance's traits. 48 """ 49 50 #: Background color when an item can be dropped on the editor: 51 ok_color = DropColor 52 53 #: The orientation of the instance editor relative to the instance selector: 54 orientation = QtGui.QBoxLayout.TopToBottom 55 56 #: Class constant: 57 extra = 0 58 59 # ------------------------------------------------------------------------- 60 # Trait definitions: 61 # ------------------------------------------------------------------------- 62 63 #: List of InstanceChoiceItem objects used by the editor 64 items = Property() 65 66 #: The view to use for displaying the instance 67 view = AView 68 69 def init(self, parent): 70 """ Finishes initializing the editor by creating the underlying toolkit 71 widget. 72 """ 73 factory = self.factory 74 if factory.name != "": 75 self._object, self._name, self._value = self.parse_extended_name( 76 factory.name 77 ) 78 79 # Create a panel to hold the object trait's view: 80 if factory.editable: 81 self.control = self._panel = parent = QtGui.QWidget() 82 83 # Build the instance selector if needed: 84 selectable = factory.selectable 85 droppable = factory.droppable 86 items = self.items 87 for item in items: 88 droppable |= item.is_droppable() 89 selectable |= item.is_selectable() 90 91 if selectable: 92 self._object_cache = {} 93 item = self.item_for(self.value) 94 if item is not None: 95 self._object_cache[id(item)] = self.value 96 97 self._choice = QtGui.QComboBox() 98 self._choice.activated.connect(self.update_object) 99 100 self.set_tooltip(self._choice) 101 102 if factory.name != "": 103 self._object.on_trait_change( 104 self.rebuild_items, self._name, dispatch="ui" 105 ) 106 self._object.on_trait_change( 107 self.rebuild_items, self._name + "_items", dispatch="ui" 108 ) 109 110 factory.on_trait_change( 111 self.rebuild_items, "values", dispatch="ui" 112 ) 113 factory.on_trait_change( 114 self.rebuild_items, "values_items", dispatch="ui" 115 ) 116 117 self.rebuild_items() 118 119 elif droppable: 120 self._choice = QtGui.QLineEdit() 121 self._choice.setReadOnly(True) 122 self.set_tooltip(self._choice) 123 124 if droppable: 125 # Install EventFilter on control to handle DND events. 126 drop_event_filter = _DropEventFilter(self.control) 127 self.control.installEventFilter(drop_event_filter) 128 129 orientation = OrientationMap[factory.orientation] 130 if orientation is None: 131 orientation = self.orientation 132 133 if (selectable or droppable) and factory.editable: 134 layout = QtGui.QBoxLayout(orientation, parent) 135 layout.setContentsMargins(0, 0, 0, 0) 136 layout.addWidget(self._choice) 137 138 if orientation == QtGui.QBoxLayout.TopToBottom: 139 hline = QtGui.QFrame() 140 hline.setFrameShape(QtGui.QFrame.HLine) 141 hline.setFrameShadow(QtGui.QFrame.Sunken) 142 143 layout.addWidget(hline) 144 145 self.create_editor(parent, layout) 146 elif self.control is None: 147 if self._choice is None: 148 self._choice = QtGui.QComboBox() 149 self._choice.activated[int].connect(self.update_object) 150 151 self.control = self._choice 152 else: 153 layout = QtGui.QBoxLayout(orientation, parent) 154 layout.setContentsMargins(0, 0, 0, 0) 155 self.create_editor(parent, layout) 156 157 # Synchronize the 'view' to use: 158 # fixme: A normal assignment can cause a crash (for unknown reasons) in 159 # some cases, so we make sure that no notifications are generated: 160 self.trait_setq(view=factory.view) 161 self.sync_value(factory.view_name, "view", "from") 162 163 def create_editor(self, parent, layout): 164 """ Creates the editor control. 165 """ 166 self._panel = QtGui.QWidget() 167 layout.addWidget(self._panel) 168 169 def _get_items(self): 170 """ Gets the current list of InstanceChoiceItem items. 171 """ 172 if self._items is not None: 173 return self._items 174 175 factory = self.factory 176 if self._value is not None: 177 values = self._value() + factory.values 178 else: 179 values = factory.values 180 181 items = [] 182 adapter = factory.adapter 183 for value in values: 184 if not isinstance(value, InstanceChoiceItem): 185 value = adapter(object=value) 186 items.append(value) 187 188 self._items = items 189 190 return items 191 192 def rebuild_items(self): 193 """ Rebuilds the object selector list. 194 """ 195 # Clear the current cached values: 196 self._items = None 197 198 # Rebuild the contents of the selector list: 199 name = -1 200 value = self.value 201 choice = self._choice 202 choice.clear() 203 for i, item in enumerate(self.items): 204 if item.is_selectable(): 205 choice.addItem(item.get_name()) 206 if item.is_compatible(value): 207 name = i 208 209 # Reselect the current item if possible: 210 if name >= 0: 211 choice.setCurrentIndex(name) 212 else: 213 # Otherwise, current value is no longer valid, try to discard it: 214 try: 215 self.value = None 216 except: 217 pass 218 219 def item_for(self, object): 220 """ Returns the InstanceChoiceItem for a specified object. 221 """ 222 for item in self.items: 223 if item.is_compatible(object): 224 return item 225 226 return None 227 228 def view_for(self, object, item): 229 """ Returns the view to use for a specified object. 230 """ 231 view = "" 232 if item is not None: 233 view = item.get_view() 234 235 if view == "": 236 view = self.view 237 238 return self.ui.handler.trait_view_for( 239 self.ui.info, view, object, self.object_name, self.name 240 ) 241 242 def update_object(self, index): 243 """ Handles the user selecting a new value from the combo box. 244 """ 245 item = self.items[index] 246 id_item = id(item) 247 object = self._object_cache.get(id_item) 248 if object is None: 249 object = item.get_object() 250 if (not self.factory.editable) and item.is_factory: 251 view = self.view_for(object, self.item_for(object)) 252 view.ui(object, self.control, "modal") 253 254 if self.factory.cachable: 255 self._object_cache[id_item] = object 256 257 self.value = object 258 self.resynch_editor() 259 260 def update_editor(self): 261 """ Updates the editor when the object trait changes externally to the 262 editor. 263 """ 264 # Synchronize the editor contents: 265 self.resynch_editor() 266 267 # Update the selector (if any): 268 choice = self._choice 269 item = self.item_for(self.value) 270 if (choice is not None) and (item is not None): 271 name = item.get_name(self.value) 272 if self._object_cache is not None: 273 idx = choice.findText(name) 274 if idx < 0: 275 idx = choice.count() 276 choice.addItem(name) 277 278 choice.setCurrentIndex(idx) 279 else: 280 choice.setText(name) 281 282 def resynch_editor(self): 283 """ Resynchronizes the contents of the editor when the object trait 284 changes externally to the editor. 285 """ 286 panel = self._panel 287 if panel is not None: 288 # Dispose of the previous contents of the panel: 289 layout = panel.layout() 290 if layout is None: 291 layout = QtGui.QVBoxLayout(panel) 292 layout.setContentsMargins(0, 0, 0, 0) 293 elif self._ui is not None: 294 self._ui.dispose() 295 self._ui = None 296 else: 297 child = layout.takeAt(0) 298 while child is not None: 299 child = layout.takeAt(0) 300 301 del child 302 303 # Create the new content for the panel: 304 stretch = 0 305 value = self.value 306 if not isinstance(value, HasTraits): 307 str_value = "" 308 if value is not None: 309 str_value = self.str_value 310 control = QtGui.QLabel(str_value) 311 else: 312 view = self.view_for(value, self.item_for(value)) 313 context = value.trait_context() 314 handler = None 315 if isinstance(value, Handler): 316 handler = value 317 context.setdefault("context", self.object) 318 context.setdefault("context_handler", self.ui.handler) 319 self._ui = ui = view.ui( 320 context, 321 panel, 322 "subpanel", 323 value.trait_view_elements(), 324 handler, 325 self.factory.id, 326 ) 327 control = ui.control 328 self.scrollable = ui._scrollable 329 ui.parent = self.ui 330 331 if view.resizable or view.scrollable or ui._scrollable: 332 stretch = 1 333 334 # FIXME: Handle stretch. 335 layout.addWidget(control) 336 337 def dispose(self): 338 """ Disposes of the contents of an editor. 339 """ 340 # Make sure we aren't hanging on to any object refs: 341 self._object_cache = None 342 343 if self._ui is not None: 344 self._ui.dispose() 345 346 if self._choice is not None: 347 if self._object is not None: 348 self._object.on_trait_change( 349 self.rebuild_items, self._name, remove=True 350 ) 351 self._object.on_trait_change( 352 self.rebuild_items, self._name + "_items", remove=True 353 ) 354 355 self.factory.on_trait_change( 356 self.rebuild_items, "values", remove=True 357 ) 358 self.factory.on_trait_change( 359 self.rebuild_items, "values_items", remove=True 360 ) 361 362 super(CustomEditor, self).dispose() 363 364 def error(self, excp): 365 """ Handles an error that occurs while setting the object's trait value. 366 """ 367 pass 368 369 def get_error_control(self): 370 """ Returns the editor's control for indicating error status. 371 """ 372 return self._choice or self.control 373 374 # -- UI preference save/restore interface --------------------------------- 375 376 def restore_prefs(self, prefs): 377 """ Restores any saved user preference information associated with the 378 editor. 379 """ 380 ui = self._ui 381 if (ui is not None) and (prefs.get("id") == ui.id): 382 ui.set_prefs(prefs.get("prefs")) 383 384 def save_prefs(self): 385 """ Returns any user preference information associated with the editor. 386 """ 387 ui = self._ui 388 if (ui is not None) and (ui.id != ""): 389 return {"id": ui.id, "prefs": ui.get_prefs()} 390 391 return None 392 393 # -- Traits event handlers ------------------------------------------------ 394 395 def _view_changed(self, view): 396 self.resynch_editor() 397 398 399class SimpleEditor(CustomEditor): 400 """ Simple style of editor for instances, which displays a button. Clicking 401 the button displays a dialog box in which the instance can be edited. 402 """ 403 404 #: The ui instance for the currently open editor dialog 405 _dialog_ui = Instance("traitsui.ui.UI") 406 407 #: Class constants: 408 orientation = QtGui.QBoxLayout.LeftToRight 409 extra = 2 410 411 def create_editor(self, parent, layout): 412 """ Creates the editor control (a button). 413 """ 414 self._button = QtGui.QPushButton() 415 layout.addWidget(self._button) 416 self._button.clicked.connect(self.edit_instance) 417 # Make sure the editor is properly disposed if parent UI is closed 418 self._button.destroyed.connect(self._parent_closed) 419 420 def edit_instance(self): 421 """ Edit the contents of the object trait when the user clicks the 422 button. 423 """ 424 # Create the user interface: 425 factory = self.factory 426 view = self.ui.handler.trait_view_for( 427 self.ui.info, factory.view, self.value, self.object_name, self.name 428 ) 429 self._dialog_ui = self.value.edit_traits( 430 view, kind=factory.kind, id=factory.id 431 ) 432 433 # Check to see if the view was 'modal', in which case it will already 434 # have been closed (i.e. is None) by the time we get control back: 435 if self._dialog_ui.control is not None: 436 # Position the window on the display: 437 position_window(self._dialog_ui.control) 438 439 # Chain our undo history to the new user interface if it does not 440 # have its own: 441 if self._dialog_ui.history is None: 442 self._dialog_ui.history = self.ui.history 443 444 else: 445 self._dialog_ui = None 446 447 def resynch_editor(self): 448 """ Resynchronizes the contents of the editor when the object trait 449 changes externally to the editor. 450 """ 451 button = self._button 452 if button is not None: 453 label = self.factory.label 454 if label == "": 455 label = user_name_for(self.name) 456 457 button.setText(label) 458 button.setEnabled(isinstance(self.value, HasTraits)) 459 460 def _parent_closed(self): 461 if self._dialog_ui is not None: 462 if self._dialog_ui.control is not None: 463 self._dialog_ui.control.close() 464 self._dialog_ui.dispose() 465 self._dialog_ui = None 466