1# ------------------------------------------------------------------------------ 2# 3# Copyright (c) 2005, Enthought, Inc. 4# All rights reserved. 5# 6# This software is provided without warranty under the terms of the BSD 7# license included in LICENSE.txt and may be redistributed only 8# under the conditions described in the aforementioned license. The license 9# is also available online at http://www.enthought.com/licenses/BSD.txt 10# 11# Thanks for using Enthought open source! 12# 13# Author: David C. Morrill 14# Date: 10/07/2004 15# 16# ------------------------------------------------------------------------------ 17 18""" Defines the Item class, which is used to represent a single item within 19 a Traits-based user interface. 20""" 21 22 23 24import re 25 26from traits.api import ( 27 Bool, 28 Callable, 29 Constant, 30 Delegate, 31 Float, 32 Instance, 33 Range, 34 Str, 35 Undefined, 36 Dict, 37) 38 39from traits.trait_base import user_name_for 40 41from .view_element import ViewSubElement 42 43from .ui_traits import ContainerDelegate, EditorStyle 44 45from .editor_factory import EditorFactory 46 47 48 49# Pattern of all digits: 50all_digits = re.compile(r"\d+") 51 52# Pattern for finding size infomation embedded in an item description: 53size_pat = re.compile(r"^(.*)<(.*)>(.*)$", re.MULTILINE | re.DOTALL) 54 55# Pattern for finding tooltip infomation embedded in an item description: 56tooltip_pat = re.compile(r"^(.*)`(.*)`(.*)$", re.MULTILINE | re.DOTALL) 57 58# ------------------------------------------------------------------------- 59# Trait definitions: 60# ------------------------------------------------------------------------- 61 62# Reference to an EditorFactory: 63ItemEditor = Instance(EditorFactory, allow_none=True) 64 65# Amount of padding to add around an item: 66Padding = Range(-15, 15, 0, desc="amount of padding to add around item") 67 68# ------------------------------------------------------------------------- 69# 'Item' class: 70# ------------------------------------------------------------------------- 71 72 73class Item(ViewSubElement): 74 """ An element in a Traits-based user interface. 75 76 Magic: 77 78 - Items are rendered as layout elements if :attr:`name` is set to 79 special values: 80 81 * ``name=''``, the item is rendered as a static label 82 83 * ``name='_'``, the item is rendered as a separator 84 85 * ``name=' '``, the item is rendered as a 5 pixel spacer 86 87 * ``name='23'`` (any number), the item is rendered as a spacer of 88 the size specified (number of pixels) 89 """ 90 91 # FIXME: all the logic for the name = '', '_', ' ', '23' magic is in 92 # _GroupPanel._add_items in qt/ui_panel.py, which is a very unlikely place 93 # to look for it. Ideally, that logic should be in this class. 94 95 # ------------------------------------------------------------------------- 96 # Trait definitions: 97 # ------------------------------------------------------------------------- 98 99 #: A unique identifier for the item. If not set, it defaults to the value 100 #: of **name**. 101 id = Str() 102 103 #: User interface label for the item in the GUI. If this attribute is not 104 #: set, the label is the value of **name** with slight modifications: 105 #: underscores are replaced by spaces, and the first letter is capitalized. 106 #: If an item's **name** is not specified, its label is displayed as 107 #: static text, without any editor widget. 108 label = Str() 109 110 #: Name of the trait the item is editing: 111 name = Str() 112 113 #: Style-sheet to apply to item / group (Qt only) 114 style_sheet = Str() 115 116 #: Help text describing the purpose of the item. The built-in help handler 117 #: displays this text in a pop-up window if the user clicks the widget's 118 #: label. View-level help displays the help text for all items in a view. 119 #: If this attribute is not set, the built-in help handler generates a 120 #: description based on the trait definition. 121 help = Str() 122 123 #: The HasTraits object whose trait attribute the item is editing: 124 object = ContainerDelegate 125 126 #: Presentation style for the item: 127 style = ContainerDelegate 128 129 #: Docking style for the item: 130 dock = ContainerDelegate 131 132 #: Image to display on notebook tabs: 133 image = ContainerDelegate 134 135 #: Category of elements dragged from view: 136 export = ContainerDelegate 137 138 #: Should a label be displayed for the item? 139 show_label = Delegate("container", "show_labels") 140 141 #: Editor to use for the item: 142 editor = ItemEditor 143 144 #: Additional editor traits to be set if default traits editor to be used: 145 editor_args = Dict() 146 147 #: Should the item use extra space along its Group's non-layout axis? If set to 148 #: True, the widget expands to fill any extra space that is available in the 149 #: display. If set to True for more than one item in the same View, any extra 150 #: space is divided between them. If set to False, the widget uses only 151 #: whatever space it is explicitly (or implicitly) assigned. The default 152 #: value of Undefined means that the use (or non-use) of extra space will be 153 #: determined by the editor associated with the item. 154 resizable = Bool(Undefined) 155 156 #: Should the item use extra space along its Group's layout axis? For 157 #: example, it a vertical group, should an item expand vertically to use 158 #: any extra space available in the group? 159 springy = Bool(False) 160 161 #: Should the item use any extra space along its Group's non-layout 162 #: orientation? For example, in a vertical group, should an item expand 163 #: horizontally to the full width of the group? If left to the default value 164 #: of Undefined, the decision will be left up to the associated item editor. 165 full_size = Bool(Undefined) 166 167 #: Should the item's label use emphasized text? If the label is not shown, 168 #: this attribute is ignored. 169 emphasized = Bool(False) 170 171 #: Should the item receive focus initially? 172 has_focus = Bool(False) 173 174 #: Pre-condition for including the item in the display. If the expression 175 #: evaluates to False, the item is not defined in the display. Conditions 176 #: for **defined_when** are evaluated only once, when the display is first 177 #: constructed. Use this attribute for conditions based on attributes that 178 #: vary from object to object, but that do not change over time. For example, 179 #: displaying a 'maiden_name' item only for female employees in a company 180 #: database. 181 defined_when = Str() 182 183 #: Pre-condition for showing the item. If the expression evaluates to False, 184 #: the widget is not visible (and disappears if it was previously visible). 185 #: If the value evaluates to True, the widget becomes visible. All 186 #: **visible_when** conditions are checked each time that any trait value 187 #: is edited in the display. Therefore, you can use **visible_when** 188 #: conditions to hide or show widgets in response to user input. 189 visible_when = Str() 190 191 #: Pre-condition for enabling the item. If the expression evaluates to False, 192 #: the widget is disabled, that is, it does not accept input. All 193 #: **enabled_when** conditions are checked each time that any trait value 194 #: is edited in the display. Therefore, you can use **enabled_when** 195 #: conditions to enable or disable widgets in response to user input. 196 enabled_when = Str() 197 198 #: Amount of extra space, in pixels, to add around the item. Values must be 199 #: integers between -15 and 15. Use negative values to subtract from the 200 #: default spacing. 201 padding = Padding 202 203 #: Tooltip to display over the item, when the mouse pointer is left idle 204 #: over the widget. Make this text as concise as possible; use the **help** 205 #: attribute to provide more detailed information. 206 tooltip = Str() 207 208 #: A Callable to use for formatting the contents of the item. This function 209 #: or method is called to create the string representation of the trait value 210 #: to be edited. If the widget does not use a string representation, this 211 #: attribute is ignored. 212 format_func = Callable() 213 214 #: Python format string to use for formatting the contents of the item. 215 #: The format string is applied to the string representation of the trait 216 #: value before it is displayed in the widget. This attribute is ignored if 217 #: the widget does not use a string representation, or if the 218 #: **format_func** is set. 219 format_str = Str() 220 221 #: Requested width of the editor (in pixels or fraction of available width). 222 #: For pixel values (i.e. values not in the range from 0.0 to 1.0), the 223 #: actual displayed width is at least the maximum of **width** and the 224 #: optimal width of the widget as calculated by the GUI toolkit. Specify a 225 #: negative value to ignore the toolkit's optimal width. For example, use 226 #: -50 to force a width of 50 pixels. The default value of -1 ensures that 227 #: the toolkit's optimal width is used. 228 #: 229 #: A value in the range from 0.0 to 1.0 specifies the fraction of the 230 #: available width to assign to the editor. Note that the value is not an 231 #: absolute value, but is relative to other item's whose **width** is also 232 #: in the 0.0 to 1.0 range. For example, if you have two item's with a width 233 #: of 0.1, and one item with a width of 0.2, the first two items will each 234 #: receive 25% of the available width, while the third item will receive 235 #: 50% of the available width. The available width is the total width of the 236 #: view minus the width of any item's with fixed pixel sizes (i.e. width 237 #: values not in the 0.0 to 1.0 range). 238 width = Float(-1.0) 239 240 #: Requested height of the editor (in pixels or fraction of available 241 #: height). For pixel values (i.e. values not in the range from 0.0 to 1.0), 242 #: the actual displayed height is at least the maximum of **height** and the 243 #: optimal height of the widget as calculated by the GUI toolkit. Specify a 244 #: negative value to ignore the toolkit's optimal height. For example, use 245 #: -50 to force a height of 50 pixels. The default value of -1 ensures that 246 #: the toolkit's optimal height is used. 247 #: 248 #: A value in the range from 0.0 to 1.0 specifies the fraction of the 249 #: available height to assign to the editor. Note that the value is not an 250 #: absolute value, but is relative to other item's whose **height** is also 251 #: in the 0.0 to 1.0 range. For example, if you have two item's with a height 252 #: of 0.1, and one item with a height of 0.2, the first two items will each 253 #: receive 25% of the available height, while the third item will receive 254 #: 50% of the available height. The available height is the total height of 255 #: the view minus the height of any item's with fixed pixel sizes (i.e. 256 #: height values not in the 0.0 to 1.0 range). 257 height = Float(-1.0) 258 259 #: The extended trait name of the trait containing the item's invalid state 260 #: status (passed through to the item's editor): 261 invalid = Str() 262 263 def __init__(self, value=None, **traits): 264 """ Initializes the item object. 265 """ 266 super(Item, self).__init__(**traits) 267 268 if value is None: 269 return 270 271 if not isinstance(value, str): 272 raise TypeError( 273 "The argument to Item must be a string of the " 274 "form: [id:][object.[object.]*][name]['['label']']`tooltip`" 275 "[<width[,height]>][#^][$|@|*|~|;style]" 276 ) 277 278 value, empty = self._parse_label(value) 279 if empty: 280 self.show_label = False 281 282 value = self._parse_style(value) 283 value = self._parse_size(value) 284 value = self._parse_tooltip(value) 285 value = self._option(value, "#", "resizable", True) 286 value = self._option(value, "^", "emphasized", True) 287 value = self._split("id", value, ":", str.find, 0, 1) 288 value = self._split("object", value, ".", str.rfind, 0, 1) 289 290 if value != "": 291 self.name = value 292 293 def is_includable(self): 294 """ Returns a Boolean indicating whether the object is replaceable by an 295 Include object. 296 """ 297 return self.id != "" 298 299 def is_spacer(self): 300 """ Returns True if the item represents a spacer or separator. 301 """ 302 name = self.name.strip() 303 304 return ( 305 (name == "") 306 or (name == "_") 307 or (all_digits.match(name) is not None) 308 ) 309 310 def get_help(self, ui): 311 """ Gets the help text associated with the Item in a specified UI. 312 """ 313 # Return 'None' if the Item is a separator or spacer: 314 if self.is_spacer(): 315 return None 316 317 # Otherwise, it must be a trait Item: 318 if self.help != "": 319 return self.help 320 321 object = eval(self.object_, globals(), ui.context) 322 323 return object.base_trait(self.name).get_help() 324 325 def get_label(self, ui): 326 """ Gets the label to use for a specified Item. 327 328 If not specified, the label is set as the name of the 329 corresponding trait, replacing '_' with ' ', and capitalizing 330 the first letter (see :func:`user_name_for`). This is called 331 the *user name*. 332 333 Magic: 334 335 - if attr:`item.label` is specified, and it begins with '...', 336 the final label is the user name followed by the item label 337 - if attr:`item.label` is specified, and it ends with '...', 338 the final label is the item label followed by the user name 339 """ 340 # Return 'None' if the Item is a separator or spacer: 341 if self.is_spacer(): 342 return None 343 344 label = self.label 345 if label != "": 346 return label 347 348 name = self.name 349 object = eval(self.object_, globals(), ui.context) 350 trait = object.base_trait(name) 351 label = user_name_for(name) 352 tlabel = trait.label 353 if tlabel is None: 354 return label 355 356 if isinstance(tlabel, str): 357 if tlabel[0:3] == "...": 358 return label + tlabel[3:] 359 if tlabel[-3:] == "...": 360 return tlabel[:-3] + label 361 if self.label != "": 362 return self.label 363 return tlabel 364 365 return tlabel(object, name, label) 366 367 def get_id(self): 368 """ Returns an ID used to identify the item. 369 """ 370 if self.id != "": 371 return self.id 372 373 return self.name 374 375 def _parse_size(self, value): 376 """ Parses a '<width,height>' value from the string definition. 377 """ 378 match = size_pat.match(value) 379 if match is not None: 380 data = match.group(2) 381 value = match.group(1) + match.group(3) 382 col = data.find(",") 383 if col < 0: 384 self._set_float("width", data) 385 else: 386 self._set_float("width", data[:col]) 387 self._set_float("height", data[col + 1 :]) 388 389 return value 390 391 def _parse_tooltip(self, value): 392 """ Parses a *tooltip* value from the string definition. 393 """ 394 match = tooltip_pat.match(value) 395 if match is not None: 396 self.tooltip = match.group(2) 397 value = match.group(1) + match.group(3) 398 399 return value 400 401 def _set_float(self, name, value): 402 """ Sets a specified trait to a specified string converted to a float. 403 """ 404 value = value.strip() 405 if value != "": 406 setattr(self, name, float(value)) 407 408 def __repr__(self): 409 """ Returns a "pretty print" version of the Item. 410 """ 411 412 options = self._repr_options( 413 "id", "object", "label", "style", "show_label", "width", "height" 414 ) 415 if options is None: 416 return "Item( '%s' )" % self.name 417 418 return "Item( '%s'\n%s\n)" % ( 419 self.name, 420 self._indent(options, " "), 421 ) 422 423 424# ------------------------------------------------------------------------- 425# 'UItem' class: 426# ------------------------------------------------------------------------- 427 428 429class UItem(Item): 430 """ An Item that has no label. 431 """ 432 433 show_label = Bool(False) 434 435 436# ------------------------------------------------------------------------- 437# 'Custom' class: 438# ------------------------------------------------------------------------- 439 440 441class Custom(Item): 442 """ An Item using a 'custom' style. 443 """ 444 445 style = EditorStyle("custom") 446 447 448# ------------------------------------------------------------------------- 449# 'UCustom' class: 450# ------------------------------------------------------------------------- 451 452 453class UCustom(Custom): 454 """ An Item using a 'custom' style with no label. 455 """ 456 457 show_label = Bool(False) 458 459 460# ------------------------------------------------------------------------- 461# 'Readonly' class: 462# ------------------------------------------------------------------------- 463 464 465class Readonly(Item): 466 """ An Item using a 'readonly' style. 467 """ 468 469 style = EditorStyle("readonly") 470 471 472# ------------------------------------------------------------------------- 473# 'UReadonly' class: 474# ------------------------------------------------------------------------- 475 476 477class UReadonly(Readonly): 478 """ An Item using a 'readonly' style with no label. 479 """ 480 481 show_label = Bool(False) 482 483 484# ------------------------------------------------------------------------- 485# 'Label' class: 486# ------------------------------------------------------------------------- 487 488 489class Label(Item): 490 """ An item that is a label. 491 """ 492 493 def __init__(self, label, **traits): 494 super(Label, self).__init__(label=label, **traits) 495 496 497# ------------------------------------------------------------------------- 498# 'Heading' class: 499# ------------------------------------------------------------------------- 500 501 502class Heading(Label): 503 """ An item that is a fancy label. 504 """ 505 506 #: Override the 'style' trait to default to the fancy 'custom' style: 507 style = Constant("custom") 508 509 510# ------------------------------------------------------------------------- 511# 'Spring' class: 512# ------------------------------------------------------------------------- 513 514 515class Spring(Item): 516 """ An item that is a layout "spring". 517 """ 518 519 # ------------------------------------------------------------------------- 520 # Trait definitions: 521 # ------------------------------------------------------------------------- 522 523 #: Name of the trait the item is editing 524 #: Just a dummy trait that exists on all HasTraits objects. It's an Event, 525 #: so it won't cause Traits UI to add any synchronization, and because it 526 #: already exists, it won't force the addition of a new trait with a bogus 527 #: name. 528 name = "trait_modified" 529 530 #: Should a label be displayed? 531 show_label = Bool(False) 532 533 #: Editor to use for the item 534 editor = Instance("traitsui.api.NullEditor", ()) 535 536 #: Should the item use extra space along its Group's layout orientation? 537 springy = True 538 539 540# A pre-defined spring for convenience 541spring = Spring() 542