class CFoo(HasTraits): ints = CList(Int) strs = CList(Str)
class DataBox(AbstractOverlay): """ An overlay that is a box defined by data space coordinates. This can be used as a base class for various kinds of zoom boxes. Unlike the "momentary" zoom box drawn for the ZoomTool, a ZoomBox is a more permanent visual component. """ data_position = Property data_bounds = Property # Should the zoom box stay attached to the image or to the screen if the # component moves underneath it? # TODO: This basically works, but the problem is that it responds to both # changes in X and Y independently. The DataRange2D needs to be updated # to reflect changes from its two DataRange1Ds. The PanTool and ZoomTool # need to be improved that they change both dimensions at once. affinity = Enum("image", "screen") #------------------------------------------------------------------------- # Appearance properties (for Box mode) #------------------------------------------------------------------------- # The color of the selection box. color = ColorTrait("lightskyblue") # The alpha value to apply to **color** when filling in the selection # region. Because it is almost certainly useless to have an opaque zoom # rectangle, but it's also extremely useful to be able to use the normal # named colors from Enable, this attribute allows the specification of a # separate alpha value that replaces the alpha value of **color** at draw # time. alpha = Trait(0.3, None, Float) # The color of the outside selection rectangle. border_color = ColorTrait("dodgerblue") # The thickness of selection rectangle border. border_size = Int(1) #------------------------------------------------------------------------- # Private Traits #------------------------------------------------------------------------- _data_position = CList([0, 0]) _data_bounds = CList([0, 0]) _position_valid = False _bounds_valid = False # Are we in the middle of an event handler or a property setter _updating = Bool(False) def __init__(self, *args, **kw): super(DataBox, self).__init__(*args, **kw) if hasattr(self.component, "range2d"): self.component.range2d._xrange.on_trait_change( self.my_component_moved, "updated") self.component.range2d._yrange.on_trait_change( self.my_component_moved, "updated") elif hasattr(self.component, "x_mapper") and hasattr( self.component, "y_mapper"): self.component.x_mapper.range.on_trait_change( self.my_component_moved, "updated") self.component.y_mapper.range.on_trait_change( self.my_component_moved, "updated") else: raise RuntimeError( "DataBox cannot find a suitable mapper on its component.") self.component.on_trait_change(self.my_component_resized, "bounds") self.component.on_trait_change(self.my_component_resized, "bounds_items") def overlay(self, component, gc, view_bounds=None, mode="normal"): if not self._position_valid: tmp = self.component.map_screen([self._data_position]) if len(tmp.shape) == 2: tmp = tmp[0] self._updating = True self.position = tmp self._updating = False self._position_valid = True if not self._bounds_valid: data_x2 = self._data_position[0] + self._data_bounds[0] data_y2 = self._data_position[1] + self._data_bounds[1] tmp = self.component.map_screen((data_x2, data_y2)) if len(tmp.shape) == 2: tmp = tmp[0] x2, y2 = tmp x, y = self.position self._updating = True self.bounds = [x2 - x, y2 - y] self._updating = False self._bounds_valid = True with gc: gc.set_antialias(0) gc.set_line_width(self.border_size) gc.set_stroke_color(self.border_color_) gc.clip_to_rect(component.x, component.y, component.width, component.height) rect = self.position + self.bounds if self.color != "transparent": if self.alpha: color = list(self.color_) if len(color) == 4: color[3] = self.alpha else: color += [self.alpha] else: color = self.color_ gc.set_fill_color(color) gc.rect(*rect) gc.draw_path() else: gc.rect(*rect) gc.stroke_path() return #------------------------------------------------------------------------- # Property setters/getters, event handlers #------------------------------------------------------------------------- def _get_data_position(self): return self._data_position def _set_data_position(self, val): self._data_position = val self._position_valid = False self.trait_property_changed("data_position", self._data_position) def _get_data_bounds(self): return self._data_bounds def _set_data_bounds(self, val): self._data_bounds = val self._bounds_valid = False self.trait_property_changed("data_bounds", self._data_bounds) @on_trait_change('position,position_items') def _update_position(self): if self._updating: return tmp = self.component.map_data(self.position) if len(tmp.shape) == 2: tmp = tmp.T[0] self._data_position = tmp self.trait_property_changed("data_position", self._data_position) @on_trait_change('bounds,bounds_items') def _update_bounds(self): if self._updating: return data_x2, data_y2 = self.component.map_data((self.x2, self.y2)) data_pos = self._data_position self._data_bounds = [data_x2 - data_pos[0], data_y2 - data_pos[1]] self.trait_property_changed("data_bounds", self._data_bounds) def my_component_moved(self): if self.affinity == "screen": # If we have screen affinity, then we need to take our current position # and map that back down into data coords self._update_position() self._update_bounds() self._bounds_valid = False self._position_valid = False def my_component_resized(self): self._bounds_valid = False self._position_valid = False
class OpenFileDialog ( Handler ): """ Defines the model and handler for the open file dialog. """ # The starting and current file path: file_name = File # The list of file filters to apply: filter = CList( Str ) # Number of history entries to allow: entries = Int( 10 ) # The file dialog title: title = Str( 'Open File' ) # The Traits UI persistence id to use: id = Str( 'traitsui.file_dialog.OpenFileDialog' ) # A list of optional file dialog extensions: extensions = CList( IFileDialogModel ) #-- Private Traits --------------------------------------------------------- # The UIInfo object for the view: info = Instance( UIInfo ) # Event fired when the file tree view should be reloaded: reload = Event # Event fired when the user double-clicks on a file name: dclick = Event # Allow extension models to be added dynamically: extension__ = Instance( IFileDialogModel ) # Is the file dialog for saving a file (or opening a file)? is_save_file = Bool( False ) # Is the currently specified file name valid? is_valid_file = Property( depends_on = 'file_name' ) # Can a directory be created now? can_create_dir = Property( depends_on = 'file_name' ) # The OK, Cancel and create directory buttons: ok = Button( 'OK' ) cancel = Button( 'Cancel' ) create = Button( image = '@icons:folder-new', style = 'toolbar' ) #-- Handler Class Method Overrides ----------------------------------------- def init_info ( self, info ): """ Handles the UIInfo object being initialized during view start-up. """ self.info = info #-- Property Implementations ----------------------------------------------- def _get_is_valid_file ( self ): if self.is_save_file: return (isfile( self.file_name ) or (not exists( self.file_name ))) return isfile( self.file_name ) def _get_can_create_dir ( self ): dir = dirname( self.file_name ) return (isdir( dir ) and access( dir, R_OK | W_OK )) #-- Handler Event Handlers ------------------------------------------------- def object_ok_changed ( self, info ): """ Handles the user clicking the OK button. """ if self.is_save_file and exists( self.file_name ): do_later( self._file_already_exists ) else: info.ui.dispose( True ) def object_cancel_changed ( self, info ): """ Handles the user clicking the Cancel button. """ info.ui.dispose( False ) def object_create_changed ( self, info ): """ Handles the user clicking the create directory button. """ if not isdir( self.file_name ): self.file_name = dirname( self.file_name ) CreateDirHandler().edit_traits( context = self, parent = info.create.control ) #-- Traits Event Handlers -------------------------------------------------- def _dclick_changed ( self ): """ Handles the user double-clicking a file name in the file tree view. """ if self.is_valid_file: self.object_ok_changed( self.info ) #-- Private Methods -------------------------------------------------------- def open_file_view ( self ): """ Returns the file dialog view to use. """ # Set up the default file dialog view and size information: item = Item( 'file_name', id = 'file_tree', style = 'custom', show_label = False, width = 0.5, editor = FileEditor( filter = self.filter, allow_dir = True, reload_name = 'reload', dclick_name = 'dclick' ) ) width = height = 0.20 # Check to see if we have any extensions being added: if len( self.extensions ) > 0: # fixme: We should use the actual values of the view's Width and # height traits here to adjust the overall width and height... width *= 2.0 # Assume we can used a fixed width Group: klass = HGroup # Set up to build a list of view Item objects: items = [] # Add each extension to the dialog: for i, extension in enumerate( self.extensions ): # Save the extension in a new trait (for use by the View): name = 'extension_%d' % i setattr( self, name, extension ) extension_view = extension # Sync up the 'file_name' trait with the extension: self.sync_trait( 'file_name', extension, mutual = True ) # Check to see if it also defines the optional IFileDialogView # interface, and if not, use the default view information: if not extension.has_traits_interface( IFileDialogView ): extension_view = default_view # Get the view that the extension wants to use: view = extension.trait_view( extension_view.view ) # Determine if we should use a splitter for the dialog: if not extension_view.is_fixed: klass = HSplit # Add the extension as a new view item: items.append( Item( name, label = user_name_for( extension.__class__.__name__ ), show_label = False, style = 'custom', width = 0.5, height = 0.5, dock = 'horizontal', resizable = True, editor = InstanceEditor( view = view, id = name ) ) ) # Finally, combine the normal view element with the extensions: item = klass( item, VSplit( id = 'splitter2', springy = True, *items ), id = 'splitter' ) # Return the resulting view: return View( VGroup( VGroup( item ), HGroup( Item( 'create', id = 'create', show_label = False, style = 'custom', defined_when = 'is_save_file', enabled_when = 'can_create_dir', tooltip = 'Create a new directory' ), Item( 'file_name', id = 'history', editor = HistoryEditor( entries = self.entries, auto_set = True ), springy = True ), Item( 'ok', id = 'ok', show_label = False, enabled_when = 'is_valid_file' ), Item( 'cancel', show_label = False ) ) ), title = self.title, id = self.id, kind = 'livemodal', width = width, height = height, close_result = False, resizable = True ) def _file_already_exists ( self ): """ Handles prompting the user when the selected file already exists, and the dialog is a 'save file' dialog. """ FileExistsHandler( message = ("The file '%s' already exists.\nDo " "you wish to overwrite it?") % basename( self.file_name ) ).edit_traits( context = self, parent = self.info.ok.control ).set( parent = self.info.ui )
class System(SystemBase): #: Name of the system (shown in WEB UI for example) name = CStr #: Allow referencing objects by their names in Callables. If disabled, you can still refer to objects by names #: by Object('name') allow_name_referencing = CBool(True) #: Filename to where to dump the system state filename = Str # LOGGING ########### #: Name of the file where logs are stored logfile = CUnicode #: Reference to logger instance (read-only) logger = Instance(logging.Logger) #: Sentry: Raven DSN configuration (see http://sentry.io) raven_dsn = Str #: Raven client (is created automatically if raven_dsn is set and this is left empty) raven_client = Instance(raven.Client, transient=True) #: Format string of the log handler that writes to stdout log_format = Str('%(asctime)s %(log_color)s%(name)s%(reset)s %(message)s') #: Format string of the log handler that writes to logfile logfile_format = Str( '%(process)d:%(threadName)s:%(name)s:%(asctime)s:%(levelname)s:%(message)s' ) #: Log level of System logger log_level = CInt(logging.INFO, transient=True) @on_trait_change('log_level', post_init=True) def log_level_changed(self, new): self.logger.setLevel(new) # SERVICES ########### #: Add here services that you want to be added automatically. This is meant to be re-defined in subclass. default_services = CList(trait=Str) #: List of services that are loaded in the initialization of the System. services = CList(trait=Instance(AbstractService)) #: List of servicenames that are desired to be avoided (even if normally autoloaded). exclude_services = CSet(trait=Str) #: Reference to the worker thread (read-only) worker_thread = Instance(StatusWorkerThread, transient=True) #: System namespace (read-only) namespace = Instance(Namespace) # Set of all SystemObjects within the system. This is where SystemObjects are ultimately stored # in the System initialization. (read-only) objects = CSet(trait=SystemObject) #: Property giving objects sorted alphabetically (read-only) objects_sorted = Property(depends_on='objects') @cached_property def _get_objects_sorted(self): return sorted(list(self.objects), key=operator.attrgetter('_order')) #: Read-only property giving all sensors of the system sensors = Property(depends_on='objects[]') @cached_property def _get_sensors(self): return { i for i in self.objects_sorted if isinstance(i, AbstractSensor) } #: Read-only property giving all actuator of the system actuators = Property(depends_on='objects[]') @cached_property def _get_actuators(self): return { i for i in self.objects_sorted if isinstance(i, AbstractActuator) } #: Read-only property giving all objects that have program features in use programs = Property(depends_on='objects[]') @cached_property def _get_programs(self): from .program import Program, DefaultProgram return { i for i in self.objects_sorted if isinstance(i, (Program, DefaultProgram)) } #: Read-only property giving all :class:`~program.Program` objects ordinary_programs = Property(depends_on='programs[]') @cached_property def _get_ordinary_programs(self): from . import program return {i for i in self.programs if isinstance(i, program.Program)} #: Start worker thread automatically after system is initialized worker_autostart = CBool(True) #: Trigger which is triggered after initialization is ready (used by Services) post_init_trigger = Event #: Trigger which is triggered before quiting (used by Services) pre_exit_trigger = Event #: Read-only property that gives list of all object tags all_tags = Property(depends_on='objects.tags[]') #: Number of state backup files num_state_backups = CInt(5) @cached_property def _get_all_tags(self): newset = set([]) for i in self.system.objects: for j in i.tags: if j: newset.add(j) return newset #: Enable experimental two-phase queue handling technique (not recommended) two_phase_queue = CBool(False) @classmethod def load_or_create(cls, filename=None, no_input=False, create_new=False, **kwargs): """ Load system from a dump, if dump file exists, or create a new system if it does not exist. """ parser = argparse.ArgumentParser() parser.add_argument('--no_input', action='store_true') parser.add_argument('--create_new', action='store_true') args = parser.parse_args() if args.no_input: print('Parameter --no_input was given') no_input = True if args.create_new: print('Parameter --create_new was given') create_new = True no_input = True def savefile_more_recent(): time_savefile = os.path.getmtime(filename) time_program = os.path.getmtime(sys.argv[0]) return time_savefile > time_program def load_pickle(): with open(filename, 'rb') as of: statefile_version, data = pickle.load(of) if statefile_version != STATEFILE_VERSION: raise RuntimeError( f'Wrong statefile version, please remove state file {filename}' ) return data def load(): print('Loading %s' % filename) obj_list, config = load_pickle() system = System(load_state=obj_list, filename=filename, **kwargs) return system def create(): print('Creating new system') config = None if filename: try: obj_list, config = load_pickle() except FileNotFoundError: config = None return cls(filename=filename, load_config=config, **kwargs) if filename and os.path.isfile(filename): if savefile_more_recent() and not create_new: return load() else: if no_input: print('Program file more recent. Loading that instead.') return create() while True: answer = input( 'Program file more recent. Do you want to load it? (y/n) ' ) if answer == 'y': return create() elif answer == 'n': return load() else: return create() def save_state(self): """ Save state of the system to a dump file :attr:`System.filename` """ if not self.filename: self.logger.error('Filename not specified. Could not save state') return self.logger.debug('Saving system state to %s', self.filename) for i in reversed(range(self.num_state_backups)): fname = self.filename if i == 0 else '%s.%d' % (self.filename, i) new_fname = '%s.%d' % (self.filename, i + 1) try: os.rename(fname, new_fname) except FileNotFoundError: pass with open(self.filename, 'wb') as file, self.worker_thread.queue.mutex: obj_list = list(self.objects) config = { obj.name: obj.status for obj in obj_list if getattr(obj, 'user_editable', False) } data = obj_list, config pickle.dump((STATEFILE_VERSION, data), file, pickle.HIGHEST_PROTOCOL) @property def cmd_namespace(self): """ A read-only property that gives the namespace of the system for evaluating commands. """ import automate ns = dict( list(automate.__dict__.items()) + list(self.namespace.items())) return ns def __getattr__(self, item): if self.namespace and item in self.namespace: return self.namespace[item] raise AttributeError def get_unique_name(self, obj, name='', name_from_system=''): """ Give unique name for an Sensor/Program/Actuator object """ ns = self.namespace newname = name if not newname: newname = name_from_system if not newname: newname = u"Nameless_" + obj.__class__.__name__ if not newname in ns: return newname counter = 0 while True: newname1 = u"%s_%.2d" % (newname, counter) if not newname1 in ns: return newname1 counter += 1 @property def services_by_name(self): """ A property that gives a dictionary that contains services as values and their names as keys. """ srvs = defaultdict(list) for i in self.services: srvs[i.__class__.__name__].append(i) return srvs @property def service_names(self): """ A property that gives the names of services as a list """ return set(self.services_by_name.keys()) def flush(self): """ Flush the worker queue. Usefull in unit tests. """ self.worker_thread.flush() def name_to_system_object(self, name): """ Give SystemObject instance corresponding to the name """ if isinstance(name, str): if self.allow_name_referencing: name = name else: raise NameError( 'System.allow_name_referencing is set to False, cannot convert string to name' ) elif isinstance(name, Object): name = str(name) return self.namespace.get(name, None) def eval_in_system_namespace(self, exec_str): """ Get Callable for specified string (for GUI-based editing) """ ns = self.cmd_namespace try: return eval(exec_str, ns) except Exception as e: self.logger.warning('Could not execute %s, gave error %s', exec_str, e) return None def register_service_functions(self, *funcs): """ Register function in the system namespace. Called by Services. """ for func in funcs: self.namespace[func.__name__] = func def register_service(self, service): """ Register service into the system. Called by Services. """ if service not in self.services: self.services.append(service) def request_service(self, type, id=0): """ Used by Sensors/Actuators/other services that need to use other services for their operations. """ srvs = self.services_by_name.get(type) if not srvs: return ser = srvs[id] if not ser.system: ser.setup_system(self, id=id) return ser def cleanup(self): """ Clean up before quitting """ self.pre_exit_trigger = True self.logger.info("Shutting down %s, please wait a moment.", self.name) for t in threading.enumerate(): if isinstance(t, TimerClass): t.cancel() self.logger.debug('Timers cancelled') for i in self.objects: i.cleanup() self.logger.debug('Sensors etc cleanups done') for ser in (i for i in self.services if isinstance(i, AbstractUserService)): ser.cleanup_system() self.logger.debug('User services cleaned up') if self.worker_thread.is_alive(): self.worker_thread.stop() self.logger.debug('Worker thread really stopped') for ser in (i for i in self.services if isinstance(i, AbstractSystemService)): ser.cleanup_system() self.logger.debug('System services cleaned up') threads = list(t.name for t in threading.enumerate() if t.is_alive() and not t.daemon) if threads: self.logger.info( 'After cleanup, we have still the following threads ' 'running: %s', ', '.join(threads)) def cmd_exec(self, cmd): """ Execute commands in automate namespace """ if not cmd: return ns = self.cmd_namespace import copy rval = True nscopy = copy.copy(ns) try: r = eval(cmd, ns) if isinstance(r, SystemObject) and not r.system: r.setup_system(self) if callable(r): r = r() cmd += "()" self.logger.info("Eval: %s", cmd) self.logger.info("Result: %s", r) except SyntaxError: r = {} try: exec(cmd, ns) self.logger.info("Exec: %s", cmd) except ExitException: raise except Exception as e: self.logger.info("Failed to exec cmd %s: %s.", cmd, e) rval = False for key, value in list(ns.items()): if key not in nscopy or not value is nscopy[key]: if key in self.namespace: del self.namespace[key] self.namespace[key] = value r[key] = value self.logger.info("Set items in namespace: %s", r) except ExitException: raise except Exception as e: self.logger.info("Failed to eval cmd %s: %s", cmd, e) return False return rval def __init__(self, load_state: 'List[SystemObject]' = None, load_config: 'Dict[str, Any]' = None, **traits): super().__init__(**traits) if not self.name: self.name = self.__class__.__name__ if self.name == 'System': self.name = os.path.split(sys.argv[0])[-1].replace('.py', '') # Initialize Sentry / raven client, if is configured if not self.raven_client and self.raven_dsn: self.raven_client = raven.Client( self.raven_dsn, release=__version__, tags={'automate-system': self.name}) self._initialize_logging() self.worker_thread = StatusWorkerThread(name="Status worker thread", system=self) self.logger.info('Initializing services') self._initialize_services() self.logger.info('Initializing namespace') self._initialize_namespace(load_state) if load_config: self.logger.info('Loading config') for obj_name, status in load_config.items(): if hasattr(self, obj_name): getattr(self, obj_name).status = status self.logger.info('Initialize user services') self._setup_user_services() if self.worker_autostart: self.logger.info('Starting worker thread') self.worker_thread.start() self.post_init_trigger = True def _initialize_logging(self): root_logger = logging.getLogger('automate') self.logger = root_logger.getChild(self.name) # Check if root level logging has been set up externally. if len(root_logger.handlers) > 0: root_logger.info('Logging has been configured already, ' 'skipping logging configuration') return root_logger.propagate = False root_logger.setLevel(self.log_level) self.logger.setLevel(self.log_level) if self.raven_client: sentry_handler = SentryHandler(client=self.raven_client, level=logging.ERROR) root_logger.addHandler(sentry_handler) if self.logfile: formatter = logging.Formatter(fmt=self.logfile_format) log_handler = logging.FileHandler(self.logfile) log_handler.setFormatter(formatter) root_logger.addHandler(log_handler) stream_handler = logging.StreamHandler() from colorlog import ColoredFormatter, default_log_colors colors = default_log_colors.copy() colors['DEBUG'] = 'purple' stream_handler.setFormatter( ColoredFormatter(self.log_format, datefmt='%H:%M:%S', log_colors=colors)) root_logger.addHandler(stream_handler) self.logger.info('Logging setup ready') def _initialize_namespace(self, load_state=None): self.namespace = Namespace(system=self) self.namespace.set_system(load_state) self.logger.info('Setup loggers per object') for name, obj in self.namespace.items(): if isinstance(obj, SystemObject): ctype = obj.__class__.__name__ obj.logger = self.logger.getChild('%s.%s' % (ctype, name)) def _initialize_services(self): # Add default_services, if not already for servname in self.default_services: if servname not in self.service_names | self.exclude_services: self.services.append(get_service_by_name(servname)()) # Add autorun services if not already for servclass in get_autoload_services(): if servclass.__name__ not in self.service_names | self.exclude_services: self.services.append(servclass()) def _setup_user_services(self): for ser in (i for i in self.services if isinstance(i, AbstractUserService)): self.logger.info('...%s', ser.__class__.__name__) ser.setup_system(self)
# A mapped trait for use in specification of line style attributes. LineStyle = Map( _line_style_trait_values, default_value='solid', editor=LineStyleEditor, ) # ----------------------------------------------------------------------------- # Trait definitions: # ----------------------------------------------------------------------------- # Font trait: font_trait = KivaFont(default_font_name) # Bounds trait bounds_trait = CList([0.0, 0.0]) # (w,h) coordinate_trait = CList([0.0, 0.0]) # (x,y) # Component minimum size trait # PZW: Make these just floats, or maybe remove them altogether. ComponentMinSize = Range(0.0, 99999.0) ComponentMaxSize = ComponentMinSize(99999.0) # Pointer shape trait: Pointer = PrefixList(pointer_shapes, default_value="arrow") # Cursor style trait: cursor_style_trait = PrefixMap(cursor_styles, default_value="default") spacing_trait = Range(0, 63, value=4) padding_trait = Range(0, 63, value=4)
STYLES = ('normal', 'italic', 'oblique') #: Font variants. Currently only small caps variants are exposed in Qt, and #: nothing in Wx. In the future this could include things like swashes, #: numeric variants, and so on, as exposed in the toolkit. VARIANTS = ['small-caps'] #: Additional markings on or around the glyphs of the font that are not part #: of the glyphs themselves. Currently Qt and Wx support underline and #: strikethrough, and Qt supports overline. In the future overlines and other #: decorations may be supported, as exposed in the toolkit. DECORATIONS = ['underline', 'strikethrough', 'overline'] #: A trait for font families. FontFamily = CList(Str, ['default']) #: A trait for font weights. FontWeight = Map(WEIGHTS, default_value='normal') #: A trait for font styles. FontStyle = Enum(STYLES) #: A trait for font variant properties. FontVariants = CSet(Enum(VARIANTS)) #: A trait for font decorator properties. FontDecorations = CSet(Enum(DECORATIONS)) class FontStretch(BaseCFloat):
class Include(BaseComponent): """ A BaseComponent subclass which allows children to be dynamically included into a parent. """ #: A read-only property which returns the toolkit widget for this #: component. This call is proxied to the parent and will return #: None if the parent does not have a toolkit widget defined. #: It is necessary to proxy the toolkit_widget so that any child #: Include components can access the proper toolkit widget to #: pass down to their dynamic components. toolkit_widget = Property #: The dynamic components of the Include. This is a component or #: list of components which should be included in the children of #: the parent of this Include. Changes made to this list in whole #: or in-place will be reflected as updates in the ui. A single #: component will be converted to a single element list. components = CList(Instance(BaseComponent)) #: A private Boolean flag indicating if the dynamic components of #: this Include have been intialized for the first time. This should #: not be manipulated directly by the user. _components_initialized = Bool(False) #-------------------------------------------------------------------------- # Property Getters and Setters #-------------------------------------------------------------------------- def _get_toolkit_widget(self): """ The property getter for the 'toolkit_widget' attribute. """ try: res = self.parent.toolkit_widget except AttributeError: res = None return res #-------------------------------------------------------------------------- # Setup Methods #-------------------------------------------------------------------------- def _setup_init_layout(self): """ A reimplemented parent class method which builds the initial list of include components during the layout initialization pass. The layout is performed bottom-up so that the tree is up-to-date before any parents compute their layout. """ super(Include, self)._setup_init_layout() self._setup_components(self.components) self.on_trait_change( self._on_components_actual_updated, 'components:_actual_updated', ) self._components_initialized = True self._actual_updated() def _setup_components(self, components): """ Run the setup process for the provided list of components. This is a private internal method which is used to run the setup process for a list of new components which should be parented by the parent widget of this Include. Parameters ---------- components : sequence The sequence of BaseComponent instances which should be setup as children of the parent widget for this Include. """ # If we get an empty sequence, don't do any unnecessary work. if not components: return # The dynamic components of an Include are injected into the # parent's children so that they *appear* as if they are # siblings of the Include. To do this, we pass the reference # to the parent of the Include and the corresponding toolkit # widget where appropriate. parent = self.parent try: # If our parent is a BaseComponent, it won't have a # toolkit_widget attribute. toolkit_parent = parent.toolkit_widget except AttributeError: toolkit_parent = None # The following blocks perform roughly the same setup process # as BaseComponent.setup(), except that we don't need to perform # the setup for this Include instance (since it's already setup). # Also, since we don't need to recurse into any children (an # Include can't have children), there is no need to break these # blocks out into separate methods. # Need to explicitly assign the parent to the components # since they were not added via the add_subcomponent method. for child in components: child.parent = self for child in components: child._setup_create_widgets(toolkit_parent) for child in components: child._setup_init_widgets() for child in components: child._setup_eval_expressions() for child in components: child._setup_bind_widgets() for child in components: child._setup_listeners() for child in components: child._setup_init_visibility() for child in components: child._setup_init_layout() for child in components: child._setup_finalize() for child in components: child._setup_set_initialized() #-------------------------------------------------------------------------- # Parent Class Overrides #-------------------------------------------------------------------------- def add_subcomponent(self, component): """ An overriden parent class method which prevents subcomponents from being declared for an Include instance. """ msg = ("Cannot add subcomponents to an Include. Assign a list of " "components to the 'components' attribute instead.") raise ValueError(msg) def get_actual(self): """ A reimplemented parent class method to include the dynamic children of this Include in our parent's list of children. """ if self._components_initialized: res = sum([c.get_actual() for c in self.components], []) else: res = [] return res def destroy(self): """ A re-implemented parent class method which destroys all of the underlying dynamic children. """ super(Include, self).destroy() for item in self.components: item.destroy() #-------------------------------------------------------------------------- # Change Handlers #-------------------------------------------------------------------------- @on_trait_change('components') def _handle_components_changed(self, obj, name, old, new): """ A private trait change handler which reacts to changes to the `components` list as a whole. """ self._update_components_diff(set(old), set(new)) @on_trait_change('components_items') def _handle_components_items_changed(self, obj, name, event): """ A private trait change handler which reacts to changes in the items of the `components` list. """ self._update_components_diff(set(event.removed), set(event.added)) def _update_components_diff(self, removed, added): """ Updates the UI components based on an item diff. A private method which will update the components by performing diff between the removed and added components. Components no longer in use will be destroyed. New components will be setup. Parameters ---------- removed : set The set of components being removed. This is allowed to overlap with `added` (e.g. a reverse operation). added : set The set of components being added. This is allowed to overlap with `removed` (e.g. a reverse operation). """ if self.initialized: to_destroy = removed - added to_setup = added - removed def closure(): for item in to_destroy: item.destroy() self._setup_components(to_setup) self._actual_updated() self.request_relayout_task(closure) # This notifier is hooked up in the '_setup_init_layout' method # due to issues surrounding trait_setq contexts. def _on_components_actual_updated(self): """ Handles a '_actual_updated' event being fired by one the dynamic components. The event is proxied up the tree by firing the same event on this instance. This allows a nested Include to update its contents independent of the Include in which it is nested. """ self._actual_updated()