class DeclaracadPlugin(Plugin): #: Project site wiki_page = Str("https;//declaracad.com/") #: Dock items to add dock_items = List(DockItem) dock_layout = Instance(AreaLayout) dock_style = Enum(*reversed(ALL_STYLES)).tag(config=True) #: Settings pages to add settings_pages = List(extensions.SettingsPage) #: Current settings page settings_page = Instance(extensions.SettingsPage) #: Internal settings models settings_model = Instance(Atom) def start(self): """ Load all the plugins declaracad is dependent on """ w = self.workbench super(DeclaracadPlugin, self).start() self._refresh_dock_items() self._refresh_settings_pages() def _bind_observers(self): """ Setup the observers for the plugin. """ super(DeclaracadPlugin, self)._bind_observers() workbench = self.workbench point = workbench.get_extension_point(extensions.DOCK_ITEM_POINT) point.observe('extensions', self._refresh_dock_items) point = workbench.get_extension_point(extensions.SETTINGS_PAGE_POINT) point.observe('extensions', self._refresh_settings_pages) def _unbind_observers(self): """ Remove the observers for the plugin. """ super(DeclaracadPlugin, self)._unbind_observers() workbench = self.workbench point = workbench.get_extension_point(extensions.DOCK_ITEM_POINT) point.unobserve('extensions', self._refresh_dock_items) point = workbench.get_extension_point(extensions.SETTINGS_PAGE_POINT) point.unobserve('extensions', self._refresh_settings_pages) # ------------------------------------------------------------------------- # Dock API # ------------------------------------------------------------------------- def create_new_area(self): """ Create the dock area """ with enaml.imports(): from .dock import DockView area = DockView(workbench=self.workbench, plugin=self) return area def get_dock_area(self): """ Get the dock area Returns ------- area: DockArea """ ui = self.workbench.get_plugin('enaml.workbench.ui') if not ui.workspace or not ui.workspace.content: ui.select_workspace('declaracad.workspace') return ui.workspace.content.find('dock_area') def _refresh_dock_items(self, change=None): """ Reload all DockItems registered by any Plugins Any plugin can add to this list by providing a DockItem extension in their PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DOCK_ITEM_POINT) #: Layout spec layout = {name: [] for name in extensions.DockItem.layout.items} dock_items = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for declaration in extension.get_children(extensions.DockItem): # Create the item DockItem = declaration.factory() plugin = workbench.get_plugin(declaration.plugin_id) item = DockItem(plugin=plugin) # Add to our layout layout[declaration.layout].append(item.name) # Save it dock_items.append(item) # Update items log.debug("Updating dock items: {}".format(dock_items)) self.dock_items = dock_items self._refresh_layout(layout) def _refresh_layout(self, layout): """ Create the layout for all the plugins """ if not self.dock_items: return AreaLayout() items = layout.pop('main') if not items: raise EnvironmentError("At least one main layout item must be " "defined!") left_items = layout.pop('main-left', []) bottom_items = layout.pop('main-bottom', []) main = TabLayout(*items) if bottom_items: main = VSplitLayout(main, *bottom_items) if left_items: main = HSplitLayout(*left_items, main) dockbars = [ DockBarLayout(*items, position=side) for side, items in layout.items() if items ] #: Update layout self.dock_layout = AreaLayout(main, dock_bars=dockbars) # ------------------------------------------------------------------------- # Settings API # ------------------------------------------------------------------------- def _default_settings_page(self): return self.settings_pages[0] def _observe_settings_page(self, change): log.debug("Settings page: {}".format(change)) def _refresh_settings_pages(self, change=None): """ Reload all SettingsPages registered by any Plugins Any plugin can add to this list by providing a SettingsPage extension in their PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.SETTINGS_PAGE_POINT) settings_pages = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for d in extension.get_children(extensions.SettingsPage): settings_pages.append(d) #: Update items settings_pages.sort(key=lambda p: p.name) log.debug("Updating settings pages: {}".format(settings_pages)) self.settings_pages = settings_pages
class Looper(Pattern): """ A pattern object that repeats its children over an iterable. The children of a `Looper` are used as a template when creating new objects for each item in the given `iterable`. Each iteration of the loop will be given an independent scope which is the union of the outer scope and any identifiers created during the iteration. This scope will also contain a `loop` variable which has `item` and `index` members to access the index and value of the iterable, respectively. All items created by the looper will be added as children of the parent of the `Looper`. The `Looper` keeps ownership of all items it creates. When the iterable for the looper is changed, the looper will only create and destroy children for the items in the iterable which have changed. When an item in the iterable is moved the `loop.index` will be updated to reflect the new index. The Looper works under the assumption that the values stored in the iterable are unique. The `loop_item` and `loop_index` scope variables are depreciated in favor of `loop.item` and `loop.index` respectively. This is because the old `loop_index` variable may become invalid when items are moved. """ #: The iterable to use when creating the items for the looper. #: The items in the iterable must be unique. This allows the #: Looper to optimize the creation and destruction of widgets. iterable = d_(Instance(Iterable)) #: The list of items created by the conditional. Each item in the #: list represents one iteration of the loop and is a list of the #: items generated during that iteration. This list should not be #: manipulated directly by user code. items = List() #: Private data storage which maps the user iterable data to the #: list of items created for that iteration. This allows the looper #: to only create and destroy the items which have changed. _iter_data = Typed(sortedmap, ()) #-------------------------------------------------------------------------- # Lifetime API #-------------------------------------------------------------------------- def destroy(self): """ A reimplemented destructor. The looper will release the owned items on destruction. """ super(Looper, self).destroy() del self.iterable del self.items del self._iter_data #-------------------------------------------------------------------------- # Observers #-------------------------------------------------------------------------- def _observe_iterable(self, change): """ A private observer for the `iterable` attribute. If the iterable changes while the looper is active, the loop items will be refreshed. """ if change['type'] == 'update' and self.is_initialized: self.refresh_items() #-------------------------------------------------------------------------- # Pattern API #-------------------------------------------------------------------------- def pattern_items(self): """ Get a list of items created by the pattern. """ return sum(self.items, []) def refresh_items(self): """ Refresh the items of the pattern. This method destroys the old items and creates and initializes the new items. """ old_items = self.items[:] old_iter_data = self._iter_data iterable = self.iterable pattern_nodes = self.pattern_nodes new_iter_data = sortedmap() new_items = [] if iterable is not None and len(pattern_nodes) > 0: for loop_index, loop_item in enumerate(iterable): iter_data = old_iter_data.get(loop_item) if iter_data is not None: new_iter_data[loop_item] = iter_data iteration = iter_data.nodes new_items.append(iteration) old_items.remove(iteration) iter_data.index = loop_index continue iter_data = Iteration(index=loop_index, item=loop_item) iteration = iter_data.nodes new_iter_data[loop_item] = iter_data new_items.append(iteration) for nodes, key, f_locals in pattern_nodes: with new_scope(key, f_locals) as f_locals: # Retain for compatibility reasons f_locals['loop_index'] = loop_index f_locals['loop_item'] = loop_item f_locals['loop'] = iter_data for node in nodes: child = node(None) if isinstance(child, list): iteration.extend(child) else: iteration.append(child) for iteration in old_items: for old in iteration: if not old.is_destroyed: old.destroy() if len(new_items) > 0: expanded = [] recursive_expand(sum(new_items, []), expanded) self.parent.insert_children(self, expanded) self.items = new_items self._iter_data = new_iter_data
class Device(Model): """ The standard device. This is a standard model used throughout the application. An instance of this is configured by specifying a 'DeviceDriver' in a plugin manifest. It simply delegates connection and handling to the selected transport and protocol respectively. """ #: Internal model for drawing the preview on screen area = Instance(AreaBase) #: The declaration that defined this device declaration = Typed(extensions.DeviceDriver).tag(config=True) #: Protocols supported by this device (ex the HPGLProtocol) protocols = List(extensions.DeviceProtocol) #: Transports supported by this device (ex the SerialPort transports = List(extensions.DeviceTransport) #: Filters that this device applies to the output filters = List(DeviceFilter).tag(config=True) #: The active transport connection = Instance(DeviceTransport).tag(config=True) #: List of jobs that were run on this device jobs = List(Model).tag(config=True) #: List of jobs queued to run on this device queue = List(Model).tag(config=True) #: Current job being processed job = Instance(Model) #.tag(config=True) #: The device specific config config = Instance(DeviceConfig, ()).tag(config=True) #: Position. Defaults to x,y,z. The protocol can #: handle this however necessary. position = ContainerList(default=[0, 0, 0]) #: Origin position. Defaults to [0, 0, 0]. The system will translate #: jobs to the origin so multiple can be run. origin = ContainerList(default=[0, 0, 0]) #: Device is currently busy processing a job busy = Bool() #: Status status = Unicode() def _default_connection(self): """ If no connection is set when the device is created, create one using the first "connection" type the driver supports. """ if not self.transports: return TestTransport() declaration = self.transports[0] driver = self.declaration protocol = self._default_protocol() return declaration.factory(driver, declaration, protocol) def _default_protocol(self): """ Create the protocol for this device. """ if not self.protocols: return DeviceProtocol() declaration = self.protocols[0] driver = self.declaration return declaration.factory(driver, declaration) def _default_area(self): """ Create the area based on the size specified by the Device Driver """ d = self.declaration area = AreaBase() w = parse_unit(d.width) if d.length: h = parse_unit(d.length) else: h = 900000 area.size = [w, h] return area @observe('declaration.width', 'declaration.length') def _refresh_area(self, change): self.area = self._default_area() @contextmanager def device_busy(self): """ Mark the device as busy """ self.busy = True try: yield finally: self.busy = False @contextmanager def device_connection(self, test=False): """ """ connection = self.connection try: #: Create a test connection if necessary if test: self.connection = TestTransport( protocol=connection.protocol, declaration=connection.declaration) #: Connect yield self.connection finally: #: Restore if necessary if test: self.connection = connection @defer.inlineCallbacks def test(self): """ Execute a test job on the device. This creates and submits new job that is simply a small square. """ raise NotImplementedError def transform(self, path): """ Apply the device output transform to the given path. This is used by other plugins that may need to display or work with tranformed output. Parameters ---------- path: QPainterPath Path to transform Returns ------- path: QPainterPath """ config = self.config t = QtGui.QTransform() #: Order matters! if config.scale: #: Do final output scaling t.scale(*config.scale) if config.rotation: #: Do final output rotation t.rotate(config.rotation) #: TODO: Translate back to 0,0 so all coordinates are positive path = path * t return path def init(self, job): """ Initialize the job. This should do any final path manipulation required by the device (or as specified by the config) and any filters should be applied here (overcut, blade offset compensation, etc..). The connection is not active at this stage. Parameters ----------- job: inkcut.job.models.Job instance The job to handle. Returns -------- model: QtGui.QPainterPath instance or Deferred that resolves to a QPainterPath if heavy processing is needed. This path is then interpolated and sent to the device. """ log.debug("device | init {}".format(job)) config = self.config #: Set the speed of this device for tracking purposes units = config.speed_units.split("/")[0] job.info.speed = from_unit(config.speed, units) #: Get the internal QPainterPath "model" model = job.model #: Transform the path to the device coordinates model = self.transform(model) if job.feed_to_end: #: Move the job to the new origin x, y, z = self.origin model.translate(x, -y) #: TODO: Apply filters here #: Return the transformed model return model @defer.inlineCallbacks def connect(self): """ Connect to the device. By default this delegates handling to the active transport or connection handler. Returns ------- result: Deferred or None May return a Deferred object that the process will wait for completion before continuing. """ log.debug("device | connect") yield defer.maybeDeferred(self.connection.connect) cmd = self.config.commands_connect if cmd: yield defer.maybeDeferred(self.connection.write, cmd) def move(self, position, absolute=True): """ Move to the given position. By default this delegates handling to the active protocol. Parameters ---------- position: List of coordinates to move to. Desired position to move or move to (if using absolute coordinates). absolute: bool Position is in absolute coordinates Returns ------- result: Deferred or None May return a deferred object that the process will wait for completion before continuing. """ if absolute: #: Clip everything to never go below zero in absolute mode position = [max(0, p) for p in position] self.position = position else: #: Convert to relative to absolute for the UI p = self.position p[0] += position[0] p[1] += position[1] self.position = p result = self.connection.protocol.move(*position, absolute=absolute) if result: return result def finish(self): """ Finish the job applying any cleanup necessary. """ log.debug("device | finish") return self.connection.protocol.finish() @defer.inlineCallbacks def disconnect(self): """ Disconnect from the device. By default this delegates handling to the active transport or connection handler. """ log.debug("device | disconnect") cmd = self.config.commands_disconnect if cmd: yield defer.maybeDeferred(self.connection.write, cmd) yield defer.maybeDeferred(self.connection.disconnect) @defer.inlineCallbacks def submit(self, job, test=False): """ Submit the job to the device. If the device is currently running a job it will be queued and run when this is finished. This handles iteration over the path model defined by the job and sending commands to the actual device using roughly the procedure is as follows: device.connect() model = device.init(job) for cmd in device.process(model): device.handle(cmd) device.finish() device.disconnect() Subclasses provided by your own DeviceDriver may reimplement this to handle path interpolation however needed. The return value is ignored. The live plot view will update whenever the device.position object is updated. On devices with lower cpu/gpu capabilities this should be updated sparingly (ie the raspberry pi). Parameters ----------- job: Instance of `inkcut.job.models.Job` The job to execute on the device test: bool Do a test run. This specifies whether the commands should be sent to the actual device or not. If True, the connection will be replaced with a virtual connection that captures all the command output. """ log.debug("device | submit {}".format(job)) try: #: Only allow one job at a time if self.busy: queue = self.queue[:] queue.append(job) self.queue = queue #: Copy and reassign so the UI updates log.info("Job {} put in device queue".format(job)) return with self.device_busy(): #: Set the current the job self.job = job self.status = "Initializing job" #: Get the time to sleep based for each unit of movement config = self.config #: Rate px/ms if config.custom_rate >= 0: rate = config.custom_rate elif self.connection.always_spools or config.spooled: rate = 0 elif config.interpolate: if config.step_time > 0: rate = config.step_size / float(config.step_time) else: rate = 0 # Undefined else: rate = from_unit( config.speed, # in/s or cm/s config.speed_units.split("/")[0]) / 1000.0 # Device model is updated in real time model = yield defer.maybeDeferred(self.init, job) #: Local references are faster info = job.info #: Determine the length for tracking progress whole_path = QtGui.QPainterPath() #: Some versions of Qt seem to require a value in #: toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) for path in model.toSubpathPolygons(m): for i, p in enumerate(path): whole_path.lineTo(p) total_length = whole_path.length() total_moved = 0 log.debug("device | Path length: {}".format(total_length)) #: So a estimate of the duration can be determined info.length = total_length info.speed = rate * 1000 #: Convert to px/s #: Waiting for approval info.status = 'waiting' #: If marked for auto approve start now if info.auto_approve: info.status = 'approved' else: #: Check for approval before starting yield defer.maybeDeferred(info.request_approval) if info.status != 'approved': self.status = "Job cancelled" return #: Update stats info.status = 'running' info.started = datetime.now() self.status = "Connecting to device" with self.device_connection(test or config.test_mode) as connection: self.status = "Processing job" try: yield defer.maybeDeferred(self.connect) #: Write startup command if config.commands_before: yield defer.maybeDeferred(connection.write, config.commands_before) self.status = "Working..." #: For point in the path for (d, cmd, args, kwargs) in self.process(model): #: Check if we paused if info.paused: self.status = "Job paused" #: Sleep until resumed, cancelled, or the #: connection drops while (info.paused and not info.cancelled and connection.connected): yield async_sleep(300) # ms #: Check for cancel for non interpolated jobs if info.cancelled: self.status = "Job cancelled" info.status = 'cancelled' break elif not connection.connected: self.status = "connection error" info.status = 'error' break #: Invoke the command #: If you want to let the device handle more complex #: commands such as curves do it in process and handle yield defer.maybeDeferred(cmd, *args, **kwargs) total_moved += d #: d should be the device must move in px #: so wait a proportional amount of time for the device #: to catch up. This avoids buffer errors from dumping #: everything at once. #: Since sending is way faster than cutting #: we must delay (without blocking the UI) before #: sending the next command or the device's buffer #: quickly gets filled and crappy china piece cutters #: get all jacked up. If the transport sends to a spooled #: output (such as a printer) this can be set to 0 if rate > 0: # log.debug("d={}, delay={} t={}".format( # d, delay, d/delay # )) yield async_sleep(d / rate) #: TODO: Check if we need to update the ui #: Set the job progress based on how far we've gone if total_length > 0: info.progress = int( max( 0, min(100, 100 * total_moved / total_length))) if info.status != 'error': #: We're done, send any finalization commands yield defer.maybeDeferred(self.finish) #: Write finalize command if config.commands_after: yield defer.maybeDeferred(connection.write, config.commands_after) #: Update stats info.ended = datetime.now() #: If not cancelled or errored if info.status == 'running': info.done = True info.status = 'complete' except Exception as e: log.error(traceback.format_exc()) raise finally: if connection.connected: yield defer.maybeDeferred(self.disconnect) #: Set the origin if job.feed_to_end and job.info.status == 'complete': self.origin = self.position #: If the user didn't cancel, set the origin and #: Process any jobs that entered the queue while this was running if self.queue and not job.info.cancelled: queue = self.queue[:] job = queue.pop(0) #: Pull the first job off the queue log.info("Rescheduling {} from queue".format(job)) self.queue = queue #: Copy and reassign so the UI updates #: Call a minute later timed_call(60000, self.submit, job) except Exception as e: log.error(' device | Execution error {}'.format( traceback.format_exc())) raise def process(self, model): """ Process the path model of a job and return each command within the job. Parameters ---------- model: QPainterPath The path to process Returns ------- generator: A list or generator object that yields each command to invoke on the device and the distance moved. In the format (distance, cmd, args, kwargs) """ config = self.config #: Previous point _p = QtCore.QPointF(self.origin[0], self.origin[1]) #: Do a final translation since Qt's y axis is reversed t = QtGui.QTransform.fromScale(1, -1) model = model * t #: Determine if interpolation should be used skip_interpolation = (self.connection.always_spools or config.spooled or not config.interpolate) # speed = distance/seconds # So distance/speed = seconds to wait step_size = config.step_size if not skip_interpolation and step_size <= 0: raise ValueError("Cannot have a step size <= 0!") try: # Apply device filters for f in self.filters: log.debug(" filter | Running {} on model".format(f)) model = f.apply_to_model(model) # Some versions of Qt seem to require a value in toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) polypath = model.toSubpathPolygons(m) # Apply device filters to polypath for f in self.filters: log.debug(" filter | Running {} on polypath".format(f)) polypath = f.apply_to_polypath(polypath) for path in polypath: #: And then each point within the path #: this is a polygon for i, p in enumerate(path): #: Head state # 0 move, 1 cut z = 0 if i == 0 else 1 #: Make a subpath subpath = QtGui.QPainterPath() subpath.moveTo(_p) subpath.lineTo(p) #: Update the last point _p = p #: Total length l = subpath.length() #: If the device does not support streaming #: the path interpolation is skipped entirely if skip_interpolation: x, y = p.x(), p.y() yield (l, self.move, ([x, y, z], ), {}) continue #: Where we are within the subpath d = 0 #: Interpolate path in steps of dl and ensure we get #: _p and p (t=0 and t=1) #: This allows us to cancel mid point while d <= l: #: Now set d to the next point by step_size #: if the end of the path is less than the step size #: use the minimum of the two dl = min(l - d, step_size) #: Now find the point at the given step size #: the first point d=0 so t=0, the last point d=l so t=1 t = subpath.percentAtLength(d) sp = subpath.pointAtPercent(t) #if d == l: # break #: Um don't we want to send the last point?? #: -y because Qt's axis is from top to bottom not bottom #: to top x, y = sp.x(), sp.y() yield (dl, self.move, ([x, y, z], ), {}) #: When we reached the end but instead of breaking above #: with a d < l we do it here to ensure we get the last #: point if d == l: #: We reached the end break #: Add step size d += dl #: Make sure we get the endpoint ep = model.currentPosition() yield (0, self.move, ([ep.x(), ep.y(), 0], ), {}) except Exception as e: log.error("device | processing error: {}".format( traceback.format_exc())) raise e def _observe_status(self, change): """ Whenever the status changes, log it """ log.info("device | {}".format(self.status)) def _observe_job(self, change): """ Save the previous jobs """ if change['type'] == 'update': job = change['value'] if job not in self.jobs: jobs = self.jobs[:] jobs.append(job) self.jobs = jobs
class AndroidHttpRequest(HttpRequest): #: okhttp3.Call that can be used to cancel the request call = Instance(Call) #: okhttp3.Request request = Instance(Request) #: Handles the async callbacks handler = Instance(BridgedAsyncHttpCallback) def init_request(self): """ Init the native request using the okhttp3.Request.Builder """ #: Build the request builder = Request.Builder() builder.url(self.url) #: Set any headers for k, v in self.headers.items(): builder.addHeader(k, v) #: Get the body or generate from the data given body = self.body if body: #: Create the request body media_type = MediaType(__id__=MediaType.parse(self.content_type)) request_body = RequestBody( __id__=RequestBody.create(media_type, body)) #: Set the request method builder.method(self.method, request_body) elif self.method in ['get', 'delete', 'head']: #: Set the method getattr(builder, self.method)() else: raise ValueError("Cannot do a '{}' request " "without a body".format(self.method)) #: Save the okhttp request self.request = Request(__id__=builder.build()) def _default_handler(self): handler = BridgedAsyncHttpCallback() handler.setAsyncHttpResponseListener( handler.getId(), self.streaming_callback is not None) handler.onStart.connect(self.on_start) handler.onCancel.connect(self.on_cancel) handler.onFailure.connect(self.on_failure) handler.onFinish.connect(self.on_finish) handler.onProgress.connect(self.on_progress) handler.onProgressData.connect(self.on_progress_data) handler.onSuccess.connect(self.on_success) handler.onRetry(self.on_retry) return handler def _default_body(self): """ If the body is not passed in by the user try to create one using the given data parameters. """ if not self.data: return "" if self.content_type == 'application/json': import json return json.dumps(self.data) elif self.content_type == 'application/x-www-form-urlencoded': import urllib return urllib.urlencode(self.data) else: raise NotImplementedError("You must manually encode the request " "body for '{}'".format( self.content_type)) def on_start(self): pass def on_cancel(self): pass def on_retry(self, retry): self.retries = retry def on_success(self, status, headers, data): r = self.response r.code = status if headers: r.headers = headers if data: r.body = data r.progress = 100 r.ok = True def on_failure(self, status, headers, data, error): r = self.response r.code = status if headers: r.headers = headers if error: r.reason = error r.error = HttpError(status, error, r) if data: r.body = data r.ok = False def on_finish(self): """ Called regardless of success or failure """ r = self.response r.request_time = time.time() - self.start_time if self.callback: self.callback(r) def on_progress(self, written, total): r = self.response r.content_length = total if total: r.progress = int(100 * written / total) def on_progress_data(self, data): if self.streaming_callback: self.streaming_callback(data)
class TreeNode (Atom): """ Represents a tree node. Used by the tree editor and tree editor factory classes. """ #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # Name of trait containing children (if '', the node is a leaf). children = Str() # Either the name of a trait containing a label, or a constant label, if # the string starts with '='. label = Unicode() # The name of a trait containing a list of labels for any columns. column_labels = Unicode() # Either the name of a trait containing a tooltip, or constant tooltip, if # the string starts with '='. tooltip = Unicode() # Name to use for a new instance name = Unicode() # Can the object's children be renamed? rename = Bool( True ) # Can the object be renamed? rename_me = Bool( True ) # Can the object's children be copied? copy = Bool( True ) # Can the object's children be deleted? delete = Bool( True ) # Can the object be deleted (if its parent allows it)? delete_me = Bool( True ) # Can children be inserted (vs. appended)? insert = Bool( True ) # Should tree nodes be automatically opened (expanded)? auto_open = Bool( False ) # Automatically close sibling tree nodes? auto_close = Bool( False ) # List of object classes than can be added or copied add = List(Value()) # List of object classes that can be moved move = List(Value()) # List of object classes and/or interfaces that the node applies to node_for = List(Value()) # Tuple of object classes that the node applies to node_for_class = Property() # List of object interfaces that the node applies to node_for_interface = Property() # Right-click context menu. The value can be one of: menu = Instance(Menu) # Name of leaf item icon icon_item = Unicode( '<item>' ) # Name of group item icon icon_group = Unicode( '<group>' ) # Name of opened group item icon icon_open = Unicode( '<open>' ) # Resource path used to locate the node icon icon_path = Unicode # Selector or name for background color background = Value('white') # Selector or name for foreground color foreground = Value('black') _py_data = Value() #--------------------------------------------------------------------------- # Initializes the object: #--------------------------------------------------------------------------- def __init__ ( self, **kwargs ): super( TreeNode, self ).__init__( **kwargs ) if self.icon_path == '': self.icon_path = 'Icon' #-- Property Implementations ----------------------------------------------- @node_for_class.getter def _get_node_for_class ( self ): return tuple([klass for klass in self.node_for]) #-- Overridable Methods: --------------------------------------------------- #--------------------------------------------------------------------------- # Returns whether chidren of this object are allowed or not: #--------------------------------------------------------------------------- def allows_children ( self, obj ): """ Returns whether this object can have children. """ return (self.children != '') #--------------------------------------------------------------------------- # Returns whether or not the object has children: #--------------------------------------------------------------------------- def has_children ( self, obj ): """ Returns whether the object has children. """ return (len( self.get_children( obj ) ) > 0) #--------------------------------------------------------------------------- # Gets the object's children: #--------------------------------------------------------------------------- def get_children ( self, obj ): """ Gets the object's children. """ return getattr( obj, self.children ) #--------------------------------------------------------------------------- # Gets the object's children identifier: #--------------------------------------------------------------------------- def get_children_id ( self, obj ): """ Gets the object's children identifier. """ return self.children #--------------------------------------------------------------------------- # Appends a child to the object's children: #--------------------------------------------------------------------------- def append_child ( self, obj, child ): """ Appends a child to the object's children. """ getattr( obj, self.children ).append( child ) #--------------------------------------------------------------------------- # Inserts a child into the object's children: #--------------------------------------------------------------------------- def insert_child ( self, obj, index, child ): """ Inserts a child into the object's children. """ getattr( obj, self.children )[ index: index ] = [ child ] #--------------------------------------------------------------------------- # Confirms that a specified object can be deleted or not: # Result = True: Delete object with no further prompting # = False: Do not delete object # = other: Take default action (may prompt user to confirm delete) #--------------------------------------------------------------------------- def confirm_delete ( self, obj ): """ Checks whether a specified object can be deleted. Returns ------- * **True** if the object should be deleted with no further prompting. * **False** if the object should not be deleted. * Anything else: Caller should take its default action (which might include prompting the user to confirm deletion). """ return None #--------------------------------------------------------------------------- # Deletes a child at a specified index from the object's children: #--------------------------------------------------------------------------- def delete_child ( self, obj, index ): """ Deletes a child at a specified index from the object's children. """ print getattr(obj, self.children) del getattr( obj, self.children )[ index ] print getattr(obj, self.children) #--------------------------------------------------------------------------- # Sets up/Tears down a listener for 'children replaced' on a specified # object: #--------------------------------------------------------------------------- # def when_children_replaced ( self, obj, listener, remove ): # """ Sets up or removes a listener for children being replaced on a # specified object. # """ ## print 'toto', obj, listener, self.children # if remove: # obj.unobserve(str(self.children), listener) # else: ## print getattr(obj, self.children) # obj.observe(str(self.children), listener) ## print obj.has_observers(self.children) # # #--------------------------------------------------------------------------- # # Sets up/Tears down a listener for 'children changed' on a specified # # object: # #--------------------------------------------------------------------------- # # def when_children_changed ( self, obj, listener, remove ): # """ Sets up or removes a listener for children being changed on a # specified object. # """ # if remove: # obj.unobserve(str(self.children), listener) # else: # obj.observe(self.children, listener) #--------------------------------------------------------------------------- # Gets the label to display for a specified object: #--------------------------------------------------------------------------- def get_label (self, obj): """ Gets the label to display for a specified object. """ label = self.label if label[:1] == '=': return label[1:] label = getattr(obj, label) return label #--------------------------------------------------------------------------- # Sets the label for a specified object: #--------------------------------------------------------------------------- def set_label ( self, obj, label ): """ Sets the label for a specified object. """ label_name = self.label if label_name[:1] != '=': setattr( obj, label_name, label ) #--------------------------------------------------------------------------- # Sets up/Tears down a listener for 'label changed' on a specified object: #--------------------------------------------------------------------------- def when_label_changed ( self, obj, listener, remove ): """ Sets up or removes a listener for the label being changed on a specified object. """ label = self.label if label[:1] != '=': if remove: obj.unobserve(label, listener) else: obj.observe(label, listener) def get_column_labels(self, obj): """ Get the labels for any columns that have been defined. """ trait = self.column_labels return [] labels = getattr(obj, trait) if not labels: labels = [] formatted = [] for formatter, label in map(None, self.column_formatters, labels): # If the list of column formatters is shorter than the list of # labels, then map(None) will extend it with Nones. Just pass the label # as preformatted. Similarly, explicitly using None in the list will # pass through the item. if formatter is None: formatted.append(label) else: formatted.append(formatter(label)) return formatted def when_column_labels_change(self, obj, listener, remove): """ Sets up or removes a listener for the column labels being changed on a specified object. This will fire when either the list is reassigned or when it is modified. I.e., it listens both to the trait change event and the trait_items change event. Implement the listener appropriately to handle either case. """ trait = self.column_labels if trait != '': if remove: obj.unobserve(trait, listener) else: obj.observe(trait, listener) #--------------------------------------------------------------------------- # Gets the tooltip to display for a specified object: #--------------------------------------------------------------------------- def get_tooltip ( self, obj ): """ Gets the tooltip to display for a specified object. """ tooltip = self.tooltip if tooltip == '': return tooltip if tooltip[:1] == '=': return tooltip[1:] tooltip = getattr( obj, tooltip) if not tooltip: tooltip = '' if self.tooltip_formatter is None: return tooltip return self.tooltip_formatter( obj, tooltip ) #--------------------------------------------------------------------------- # Returns the icon for a specified object: #--------------------------------------------------------------------------- def get_icon ( self, obj, is_expanded ): """ Returns the icon for a specified object. """ if not self.allows_children( obj ): return self.icon_item if is_expanded: return self.icon_open return self.icon_group #--------------------------------------------------------------------------- # Returns the path used to locate an object's icon: #--------------------------------------------------------------------------- def get_icon_path (self, obj): """ Returns the path used to locate an object's icon. """ return self.icon_path #--------------------------------------------------------------------------- # Returns the name to use when adding a new object instance (displayed in # the 'New' submenu): #--------------------------------------------------------------------------- def get_name (self, obj): """ Returns the name to use when adding a new object instance (displayed in the "New" submenu). """ return self.name #--------------------------------------------------------------------------- # Returns the right-click context menu for an object: #--------------------------------------------------------------------------- def get_menu ( self, context): """ Returns the right-click context menu for an object. """ if self.menu: self.menu.context = context return self.menu else: return None def get_background(self, obj) : background = self.background if isinstance(background, basestring) : background = getattr(obj, background, background) return background def get_foreground(self, obj) : foreground = self.foreground if isinstance(foreground, basestring) : foreground = getattr(obj, foreground, foreground) return foreground #--------------------------------------------------------------------------- # Returns whether or not the object's children can be renamed: #--------------------------------------------------------------------------- def can_rename ( self, obj ): """ Returns whether the object's children can be renamed. """ return self.rename #--------------------------------------------------------------------------- # Returns whether or not the object can be renamed: #--------------------------------------------------------------------------- def can_rename_me ( self, obj ): """ Returns whether the object can be renamed. """ return self.rename_me #--------------------------------------------------------------------------- # Returns whether or not the object's children can be copied: #--------------------------------------------------------------------------- def can_copy ( self, obj ): """ Returns whether the object's children can be copied. """ return self.copy #--------------------------------------------------------------------------- # Returns whether or not the object's children can be deleted: #--------------------------------------------------------------------------- def can_delete ( self, obj ): """ Returns whether the object's children can be deleted. """ return self.delete #--------------------------------------------------------------------------- # Returns whether or not the object can be deleted: #--------------------------------------------------------------------------- def can_delete_me ( self, obj ): """ Returns whether the object can be deleted. """ return self.delete_me #--------------------------------------------------------------------------- # Returns whether or not the object's children can be inserted (or just # appended): #--------------------------------------------------------------------------- def can_insert ( self, obj ): """ Returns whether the object's children can be inserted (vs. appended). """ return self.insert #--------------------------------------------------------------------------- # Returns whether or not the object's children should be auto-opened: #--------------------------------------------------------------------------- def can_auto_open ( self, obj ): """ Returns whether the object's children should be automatically opened. """ return self.auto_open #--------------------------------------------------------------------------- # Returns whether or not the object's children should be auto-closed: #--------------------------------------------------------------------------- def can_auto_close ( self, obj ): """ Returns whether the object's children should be automatically closed. """ return self.auto_close #--------------------------------------------------------------------------- # Returns whether or not this is the node that should handle a specified # object: #--------------------------------------------------------------------------- def is_node_for ( self, obj ): """ Returns whether this is the node that handles a specified object. """ return (isinstance(obj, self.node_for_class)) #--------------------------------------------------------------------------- # Returns whether a given 'add_object' can be added to an object: #--------------------------------------------------------------------------- def can_add ( self, obj, add_object ): """ Returns whether a given object is droppable on the node. """ klass = self._class_for( add_object ) if self.is_addable( klass ): return True for item in self.move: if type( item ) in (List, ContainerList, Dict): item = item[0] if issubclass( klass, item ): return True return False #--------------------------------------------------------------------------- # Returns the list of classes that can be added to the object: #--------------------------------------------------------------------------- def get_add ( self, obj ): """ Returns the list of classes that can be added to the object. """ return self.add #--------------------------------------------------------------------------- # Returns the 'draggable' version of a specified object: #--------------------------------------------------------------------------- def get_drag_object ( self, obj ): """ Returns a draggable version of a specified object. """ return obj #--------------------------------------------------------------------------- # Returns a droppable version of a specified object: #--------------------------------------------------------------------------- def drop_object ( self, obj, dropped_object ): """ Returns a droppable version of a specified object. """ klass = self._class_for( dropped_object ) if self.is_addable( klass ): return dropped_object for item in self.move: if type( item ) in (List, ContainerList, Dict): if issubclass( klass, item[0] ): return item[1]( obj, dropped_object ) elif issubclass( klass, item ): return dropped_object return dropped_object #--------------------------------------------------------------------------- # Handles an object being selected: #--------------------------------------------------------------------------- def select ( self, obj ): """ Handles an object being selected. """ # if self.on_select is not None: # self.on_select( obj ) # return None return True # #--------------------------------------------------------------------------- # # Handles an object being clicked: # #--------------------------------------------------------------------------- # # def click ( self, obj ): # """ Handles an object being clicked. # """ # if self.on_click is not None: # self.on_click( obj ) # return None # # return True # # #--------------------------------------------------------------------------- # # Handles an object being double-clicked: # #--------------------------------------------------------------------------- # # def dclick ( self, obj): # """ Handles an object being double-clicked. # """ # if self.on_dclick is not None: # self.on_dclick( obj) # return None # # return True # # #--------------------------------------------------------------------------- # # Handles an object being activated: # #--------------------------------------------------------------------------- # # def activated ( self, object ): # """ Handles an object being activated. # """ # if self.on_activated is not None: # self.on_activated( object ) # return None # # return True #--------------------------------------------------------------------------- # Returns whether or not a specified object class can be added to the node: #--------------------------------------------------------------------------- def is_addable ( self, klass ): """ Returns whether a specified object class can be added to the node. """ for item in self.add: if type( item ) in (List, ContainerList, Dict): item = item[0] if issubclass( klass, item ): return True return False #--------------------------------------------------------------------------- # Returns the class of an object: #--------------------------------------------------------------------------- def _class_for ( self, obj ): """ Returns the class of an object. """ if isinstance( obj, type ): return obj return obj.__class__
class JobInfo(Model): """ Job metadata """ #: Controls done = Bool() cancelled = Bool() paused = Bool() #: Flags status = Enum('staged', 'waiting', 'running', 'error', 'approved', 'cancelled', 'complete').tag(config=True) #: Stats created = Instance(datetime).tag(config=True) started = Instance(datetime).tag(config=True) ended = Instance(datetime).tag(config=True) progress = Range(0, 100, 0).tag(config=True) data = Str().tag(config=True) count = Int().tag(config=True) #: Device speed in px/s speed = Float(strict=False).tag(config=True) #: Length in px length = Float(strict=False).tag(config=True) #: Estimates based on length and speed duration = Instance(timedelta, ()).tag(config=True) #: Units units = Enum('in', 'cm', 'm', 'ft').tag(config=True) #: Callback to open the approval dialog auto_approve = Bool().tag(config=True) request_approval = Callable() def __init__(self, *args, **kwargs): super(JobInfo, self).__init__(*args, **kwargs) self.created = self._default_created() def _default_created(self): return datetime.now() def _default_request_approval(self): """ Request approval using the current job """ from inkcut.core.workbench import InkcutWorkbench workbench = InkcutWorkbench.instance() plugin = workbench.get_plugin("inkcut.job") return lambda: plugin.request_approval(plugin.job) def reset(self): """ Reset to initial states""" #: TODO: This is a stupid design self.progress = 0 self.paused = False self.cancelled = False self.done = False self.status = 'staged' def _observe_done(self, change): if change['type'] == 'update': #: Increment count every time it's completed if self.done: self.count += 1 @observe('length', 'speed') def _update_duration(self, change): if not self.length or not self.speed: self.duration = timedelta() return dt = self.length / self.speed self.duration = timedelta(seconds=dt)
class ParallelTransport(RawFdTransport): """ This is just a wrapper for the RawFdTransport """ #: Default config config = Instance(ParallelConfig, ()).tag(config=True)
class Plot1D(Atom): """plot attribute on dataobject""" updated = Event(kind=bool) x = Property() y = Property() _x_dict = Instance(PlotFunctionsDict) #todo make this public? _y_dict = Instance(PlotFunctionsDict) parent = ForwardInstance(lambda: ap.data.XYDataObject) line = Instance(Line2D) norm = Bool(False) zero = Bool(False) def __init__(self, parent, *args, **kwargs): super(Plot1D, self).__init__(*args, **kwargs) self.parent = parent self._x_dict = PlotFunctionsDict({}) self._y_dict = PlotFunctionsDict({}) def _get_x(self): x = np.copy(self.parent.x) result = self._apply_functions(self._x_dict, x) return result def _get_y(self): y = np.copy(self.parent.y) result = self._apply_functions(self._y_dict, y) return result @observe('norm') def _norm_changed(self, change): if change['value']: #todo needs to be diffent for normalizing the fit, should be the same factor. but how!?!? -> same functions on fit! self._y_dict['_norm'] = (lambda y: y / y.max(), [], {}) else: del self._y_dict['_norm'] self._update_y('') @observe('zero') def _zero_changed(self, change): if change['value']: self._y_dict['_zero'] = (lambda x: x - x.min(), [], {}) self._y_dict.move_to_end( '_zero', last=False) # Move zeroing to be the first operation else: del self._y_dict['_zero'] self._update_y('') @staticmethod def _apply_functions(functions, result): if functions: for f_key, content in functions.items(): func, args, kwargs = content # todo make args kwargs optional result = func(result, *args, **kwargs) return result @observe('parent.x_updated') def _update_x(self): print('update in in plot1d') self.line.set_xdata(self.x) @observe(['parent.y_updated', 'parent.y']) def _update_y(self, new): print('update y in plot1d') print(self.y) if self.line: self.line.set_ydata(self.y) self.updated = True
v = member mode = (DefaultValue.MemberMethod_Object if isinstance( member, ForwardSubclass) else DefaultValue.Static) assert StaticTest.v.default_value_mode[0] == mode assert StaticTest().v == expected assert StaticTest.v.default_value_mode[0] == DefaultValue.Static @pytest.mark.parametrize( "member, expect_error", [ (Typed(int), False), (Typed(int, (), optional=False), False), (Typed(int, factory=lambda: 1, optional=False), False), (Instance(int), False), (Instance(int, (), optional=False), False), (Instance(int, factory=lambda: 1, optional=False), False), (Instance(int, optional=False), True), (ForwardTyped(lambda: int), False), (ForwardTyped(lambda: int, (), optional=False), False), (ForwardTyped(lambda: int, factory=lambda: 1, optional=False), False), (ForwardTyped(lambda: int, optional=False), True), (ForwardInstance(lambda: int), False), (ForwardInstance(lambda: int, (), optional=False), False), (ForwardInstance(lambda: int, factory=lambda: 1, optional=False), False), (ForwardInstance(lambda: int, optional=False), True), ], ) def test_non_optional_handler(member, expect_error):
class Application(QtApplication): """ Add asyncio support . Seems like a complete hack compared to twisted but whatever. """ loop = Instance(QEventLoop) queue = Instance(Queue, ()) running = Bool() def __init__(self): super().__init__() self.loop = QEventLoop(self._qapp) asyncio.set_event_loop(self.loop) for name in ('asyncqt._unix._Selector', 'asyncqt._QEventLoop', 'asyncqt._SimpleTimer'): log = logging.getLogger(name) log.setLevel(logging.WARN) def start(self): """ Run using the event loop """ log.info("Application starting") self.running = True with self.loop: try: self.loop.run_until_complete(self.main()) except RuntimeError as e: if 'loop stopped' not in str(e): raise async def main(self): """ Run any async deferred calls in the main ui loop. """ while self.running: try: task = self.queue.get(block=False) await task except Empty: self.process_events() await asyncio.sleep(0.1) except Exception as e: if 'cannot enter context' in str(e): # HACK: Something is jacked up await asyncio.sleep(0.1) continue log.exception(e) def process_events(self): """ Let the the app process events during long-running cpu intensive tasks. """ self._qapp.processEvents() def deferred_call(self, callback, *args, **kwargs): """ Invoke a callable on the next cycle of the main event loop thread. Parameters ---------- callback : callable The callable object to execute at some point in the future. args, kwargs Any additional positional and keyword arguments to pass to the callback. """ if asyncio.iscoroutinefunction(callback) or kwargs.pop('async_', None): task = asyncio.create_task(callback(*args, **kwargs)) return self.add_task(task) return super().deferred_call(callback, *args, **kwargs) def timed_call(self, ms, callback, *args, **kwargs): """ Invoke a callable on the main event loop thread at a specified time in the future. Parameters ---------- ms : int The time to delay, in milliseconds, before executing the callable. callback : callable The callable object to execute at some point in the future. args, kwargs Any additional positional and keyword arguments to pass to the callback. """ if asyncio.iscoroutinefunction(callback) or kwargs.pop('async_', None): task = asyncio.create_task(callback(*args, **kwargs)) return super().timed_call(ms, self.add_task, task) return super().timed_call(ms, callback, *args, **kwargs) def add_task(self, task): """ Put a task into the queue """ self.queue.put(task)
class AndroidView(AndroidToolkitObject, ProxyView): """ An Android implementation of an Enaml ProxyView. """ #: A reference to the widget created by the proxy. widget = Typed(View) #: Display metrics density dp = Float(1.0) #: Layout type layout_param_type = Subclass(LayoutParams) #: Layout params layout_params = Instance(LayoutParams) #: Default layout params default_layout = Dict(default={ 'width': 'wrap_content', 'height': 'wrap_content' }) def _default_dp(self): return self.get_context().dp # ------------------------------------------------------------------------- # Initialization API # ------------------------------------------------------------------------- def create_widget(self): """ Create the underlying label widget. """ self.widget = View(self.get_context()) def init_widget(self): """ Initialize the underlying widget. This reads all items declared in the enamldef block for this node and sets only the values that have been specified. All other values will be left as default. Doing it this way makes atom to only create the properties that need to be overridden from defaults thus greatly reducing the number of initialization checks, saving time and memory. If you don't want this to happen override `get_declared_keys` to return an empty list. """ super(AndroidView, self).init_widget() # Initialize the widget by updating only the members that # have read expressions declared. This saves a lot of time and # simplifies widget initialization code for k, v in self.get_declared_items(): handler = getattr(self, 'set_'+k, None) if handler: handler(v) def get_declared_items(self): """ Get the members that were set in the enamldef block for this Declaration. Layout keys are grouped together until the end so as to avoid triggering multiple updates. Returns ------- result: List of (k,v) pairs that were defined for this widget in enaml List of keys and values """ d = self.declaration engine = d._d_engine if engine: layout = {} for k, h in engine._handlers.items(): # Handlers with read operations if not h.read_pair: continue v = getattr(d, k) if k in LAYOUT_KEYS: layout[k] = v continue yield (k, v) if layout: yield ('layout', layout) # ------------------------------------------------------------------------- # OnClickListener API # ------------------------------------------------------------------------- def on_click(self, view): """ Trigger the click """ d = self.declaration d.clicked() # ------------------------------------------------------------------------- # OnKeyListener API # ------------------------------------------------------------------------- def on_key(self, view, key, event): """ Trigger the key event Parameters ---------- view: int The ID of the view that sent this event key: int The code of the key that was pressed data: bytes The msgpack encoded key event """ d = self.declaration r = {'key': key, 'result': False} d.key_event(r) return r['result'] # ------------------------------------------------------------------------- # OnTouchListener API # ------------------------------------------------------------------------- def on_touch(self, view, event): """ Trigger the touch event Parameters ---------- view: int The ID of the view that sent this event data: bytes The msgpack encoded key event """ d = self.declaration r = {'event': event, 'result': False} d.touch_event(r) return r['result'] # ------------------------------------------------------------------------- # ProxyView API # ------------------------------------------------------------------------- def set_touch_events(self, enabled): w = self.widget if enabled: w.setOnTouchListener(w.getId()) w.onTouch.connect(self.on_touch) else: w.onTouch.disconnect(self.on_touch) def set_key_events(self, enabled): w = self.widget if enabled: w.setOnKeyListener(w.getId()) w.onKey.connect(self.on_key) else: w.onKey.disconnect(self.on_key) def set_clickable(self, clickable): w = self.widget if clickable: w.setOnClickListener(w.getId()) w.onClick.connect(self.on_click) else: w.onClick.disconnect(self.on_click) w.setClickable(clickable) def set_enabled(self, enabled): """ Set the enabled state of the widget. """ self.widget.setEnabled(enabled) def set_visible(self, visible): """ Set the visibility of the widget. """ v = View.VISIBILITY_VISIBLE if visible else View.VISIBILITY_GONE self.widget.setVisibility(v) # ------------------------------------------------------------------------- # Style updates # ------------------------------------------------------------------------- def set_background_color(self, color): """ Set the background color of the widget. """ self.widget.setBackgroundColor(color) def set_alpha(self, alpha): """ Sets the alpha or opacity of the widget. """ self.widget.setAlpha(alpha) # ------------------------------------------------------------------------- # Layout updates # ------------------------------------------------------------------------- def set_layout(self, layout): """ Sets the LayoutParams of this widget. Since the available properties that may be set for the layout params depends on the parent, actual creation of the params is delegated to the parent Parameters ---------- layout: Dict A dict of layout parameters the parent should used to layout this child. The widget defaults are updated with user passed values. """ # Update the layout with the widget defaults update = self.layout_params is not None params = self.default_layout.copy() params.update(layout) # Create the layout params parent = self.parent() if not isinstance(parent, AndroidView): # Root node parent = self update = True parent.apply_layout(self, params) if update: self.widget.setLayoutParams(self.layout_params) def update_layout(self, **params): """ Updates the LayoutParams of this widget. This delegates to the parent and expects the parent to update the existing layout without recreating it. Parameters ---------- params: Dict A dict of layout parameters the parent should used to layout this child. The widget defaults are updated with user passed values. """ self.parent().apply_layout(self, params) def create_layout_params(self, child, layout): """ Create the LayoutParams for a child with it's requested layout parameters. Subclasses should override this as needed to handle layout specific needs. Parameters ---------- child: AndroidView A view to create layout params for. layout: Dict A dict of layout parameters to use to create the layout. Returns ------- layout_params: LayoutParams A LayoutParams bridge object with the requested layout options. """ dp = self.dp w, h = (coerce_size(layout.get('width', 'wrap_content')), coerce_size(layout.get('height', 'wrap_content'))) w = w if w < 0 else int(w * dp) h = h if h < 0 else int(h * dp) layout_params = self.layout_param_type(w, h) if layout.get('margin'): l, t, r, b = layout['margin'] layout_params.setMargins(int(l*dp), int(t*dp), int(b*dp), int(r*dp)) return layout_params def apply_layout(self, child, layout): """ Apply a layout to a child. This sets the layout_params of the child which is later used during the `init_layout` pass. Subclasses should override this as needed to handle layout specific needs of the ViewGroup. Parameters ---------- child: AndroidView A view to create layout params for. layout: Dict A dict of layout parameters to use to create the layout. """ layout_params = child.layout_params if not layout_params: layout_params = self.create_layout_params(child, layout) w = child.widget if w: dp = self.dp # padding if 'padding' in layout: l, t, r, b = layout['padding'] w.setPadding(int(l*dp), int(t*dp), int(b*dp), int(r*dp)) # left, top, right, bottom if 'left' in layout: w.setLeft(int(layout['left']*dp)) if 'top' in layout: w.setTop(int(layout['top']*dp)) if 'right' in layout: w.setRight(int(layout['right']*dp)) if 'bottom' in layout: w.setBottom(int(layout['bottom']*dp)) # x, y, z if 'x' in layout: w.setX(layout['x']*dp) if 'y' in layout: w.setY(layout['y']*dp) if 'z' in layout: w.setZ(layout['z']*dp) # set min width and height # maximum is not supported by AndroidViews (without flexbox) if 'min_height' in layout: w.setMinimumHeight(int(layout['min_height']*dp)) if 'min_width' in layout: w.setMinimumWidth(int(layout['min_width']*dp)) child.layout_params = layout_params def set_width(self, width): self.update_layout(width=width) def set_height(self, height): self.update_layout(height=height) def set_padding(self, padding): self.update_layout(padding=padding) def set_margin(self, margin): self.update_layout(margin=margin) def set_x(self, x): self.update_layout(x=x) def set_y(self, y): self.update_layout(y=y) def set_z(self, z): self.update_layout(z=z) def set_top(self, top): self.update_layout(top=top) def set_left(self, left): self.update_layout(left=left) def set_right(self, right): self.update_layout(right=right) def set_bottom(self, bottom): self.update_layout(bottom=bottom) def set_gravity(self, gravity): self.update_layout(gravity=gravity) def set_min_height(self, min_height): self.update_layout(min_height=min_height) def set_max_height(self, max_height): self.update_layout(max_height=max_height) def set_min_width(self, min_width): self.update_layout(min_width=min_width) def set_max_width(self, max_width): self.update_layout(max_width=max_width) def set_flex_grow(self, flex_grow): self.update_layout(flex_grow=flex_grow) def set_flex_basis(self, flex_basis): self.update_layout(flex_basis=flex_basis) def set_flex_shrink(self, flex_shrink): self.update_layout(flex_shrink=flex_shrink) def set_align_self(self, align_self): self.update_layout(align_self=align_self)
class Future(Atom, object): """Placeholder for an asynchronous result. A ``Future`` encapsulates the result of an asynchronous operation. In synchronous applications ``Futures`` are used to wait for the result from a thread or process pool; in Tornado they are normally used with `.IOLoop.add_future` or by yielding them in a `.gen.coroutine`. `tornado.concurrent.Future` is similar to `concurrent.futures.Future`, but not thread-safe (and therefore faster for use with single-threaded event loops). In addition to ``exception`` and ``set_exception``, methods ``exc_info`` and ``set_exc_info`` are supported to capture tracebacks in Python 2. The traceback is automatically available in Python 3, but in the Python 2 futures backport this information is discarded. This functionality was previously available in a separate class ``TracebackFuture``, which is now a deprecated alias for this class. .. versionchanged:: 4.0 `tornado.concurrent.Future` is always a thread-unsafe ``Future`` with support for the ``exc_info`` methods. Previously it would be an alias for the thread-safe `concurrent.futures.Future` if that package was available and fall back to the thread-unsafe implementation if it was not. .. versionchanged:: 4.1 If a `.Future` contains an error but that error is never observed (by calling ``result()``, ``exception()``, or ``exc_info()``), a stack trace will be logged when the `.Future` is garbage collected. This normally indicates an error in the application, but in cases where it results in undesired logging it may be necessary to suppress the logging by ensuring that the exception is observed: ``f.add_done_callback(lambda f: f.exception())``. """ __slots__ = ('__weakref__', ) _done = Bool() _result = Value() _exc_info = Value() _log_traceback = Bool() _tb_logger = Value() _callbacks = Instance(list, ()) __id__ = Int() then = Callable() catch = Callable() # Implement the Python 3.5 Awaitable protocol if possible # (we can't use return and yield together until py33). if sys.version_info >= (3, 3): exec( textwrap.dedent(""" def __await__(self): return (yield self) """)) else: # Py2-compatible version for use with cython. def __await__(self): result = yield self # StopIteration doesn't take args before py33, # but Cython recognizes the args tuple. e = StopIteration() e.args = (result, ) raise e def cancel(self): """Cancel the operation, if possible. Tornado ``Futures`` do not support cancellation, so this method always returns False. """ return False def cancelled(self): """Returns True if the operation has been cancelled. Tornado ``Futures`` do not support cancellation, so this method always returns False. """ return False def running(self): """Returns True if this operation is currently running.""" return not self._done def done(self): """Returns True if the future has finished running.""" return self._done def _clear_tb_log(self): self._log_traceback = False if self._tb_logger is not None: self._tb_logger.clear() self._tb_logger = None def result(self, timeout=None): """If the operation succeeded, return its result. If it failed, re-raise its exception. This method takes a ``timeout`` argument for compatibility with `concurrent.futures.Future` but it is an error to call it before the `Future` is done, so the ``timeout`` is never used. """ self._clear_tb_log() if self._result is not None: return self._result if self._exc_info is not None: try: raise_exc_info(self._exc_info) finally: self = None self._check_done() return self._result def exception(self, timeout=None): """If the operation raised an exception, return the `Exception` object. Otherwise returns None. This method takes a ``timeout`` argument for compatibility with `concurrent.futures.Future` but it is an error to call it before the `Future` is done, so the ``timeout`` is never used. """ self._clear_tb_log() if self._exc_info is not None: return self._exc_info[1] else: self._check_done() return None def add_done_callback(self, fn): """Attaches the given callback to the `Future`. It will be invoked with the `Future` as its argument when the Future has finished running and its result is available. In Tornado consider using `.IOLoop.add_future` instead of calling `add_done_callback` directly. """ if self._done: fn(self) else: self._callbacks.append(fn) def set_result(self, result): """Sets the result of a ``Future``. It is undefined to call any of the ``set`` methods more than once on the same object. """ self._result = result self._set_done() def set_exception(self, exception): """Sets the exception of a ``Future.``""" self.set_exc_info((exception.__class__, exception, getattr(exception, '__traceback__', None))) def exc_info(self): """Returns a tuple in the same format as `sys.exc_info` or None. .. versionadded:: 4.0 """ self._clear_tb_log() return self._exc_info def set_exc_info(self, exc_info): """Sets the exception information of a ``Future.`` Preserves tracebacks on Python 2. .. versionadded:: 4.0 """ self._exc_info = exc_info self._log_traceback = True if not _GC_CYCLE_FINALIZERS: self._tb_logger = _TracebackLogger(exc_info) try: self._set_done() finally: # Activate the logger after all callbacks have had a # chance to call result() or exception(). if self._log_traceback and self._tb_logger is not None: self._tb_logger.activate() self._exc_info = exc_info def _check_done(self): if not self._done: raise Exception( "DummyFuture does not support blocking for results") def _set_done(self): self._done = True for cb in self._callbacks: try: cb(self) except Exception: app_log.exception('Exception in callback %r for %r', cb, self) self._callbacks = None # On Python 3.3 or older, objects with a destructor part of a reference # cycle are never destroyed. It's no longer the case on Python 3.4 thanks to # the PEP 442. if _GC_CYCLE_FINALIZERS: def __del__(self, is_finalizing=is_finalizing): if is_finalizing() or not self._log_traceback: # set_exception() was not called, or result() or exception() # has consumed the exception return tb = traceback.format_exception(*self._exc_info) app_log.error('Future %r exception was never retrieved: %s', self, ''.join(tb).rstrip())
class Job(Model): """ Create a plot depending on the properties set. Any property that is a traitlet will cause an update when the value is changed. """ #: Material this job will be run on material = Instance(Material, ()).tag(config=True) #: Path to svg document this job parses document = Unicode().tag(config=True) #: Nodes to restrict document_kwargs = Dict().tag(config=True) #: Meta info a the job info = Instance(JobInfo, ()).tag(config=True) # Job properties used for generating the plot size = ContainerList(Float(), default=[1, 1]) scale = ContainerList(Float(), default=[1, 1]).tag(config=True) auto_scale = Bool(False).tag( config=True, help="automatically scale if it's too big for the area") lock_scale = Bool(True).tag( config=True, help="automatically scale if it's too big for the area") mirror = ContainerList(Bool(), default=[False, False]).tag(config=True) align_center = ContainerList(Bool(), default=[False, False]).tag(config=True) rotation = Float(0).tag(config=True) auto_rotate = Bool(False).tag( config=True, help="automatically rotate if it saves space") copies = Int(1).tag(config=True) auto_copies = Bool(False).tag(config=True, help="always use a full stack") copy_spacing = ContainerList(Float(), default=[10, 10]).tag(config=True) copy_weedline = Bool(False).tag(config=True) copy_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) plot_weedline = Bool(False).tag(config=True) plot_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) order = Enum(*sorted(ordering.REGISTRY.keys())).tag(config=True) def _default_order(self): return 'Normal' feed_to_end = Bool(False).tag(config=True) feed_after = Float(0).tag(config=True) stack_size = ContainerList(Int(), default=[0, 0]) path = Instance(QtGui.QPainterPath) # Original path model = Instance(QtGui.QPainterPath) # Copy using job properties _blocked = Bool(False) # block change events _desired_copies = Int(1) # required for auto copies def __setstate__(self, *args, **kwargs): """ Ensure that when restoring from disk the material and info are not set to None. Ideally these would be defined as Typed but the material may be made extendable at some point. """ super(Job, self).__setstate__(*args, **kwargs) if not self.info: self.info = JobInfo() if not self.material: self.material = Material() def _observe_document(self, change): """ Read the document from stdin """ if change['type'] == 'update' and self.document == '-': #: Only load from stdin when explicitly changed to it (when doing #: open from the cli) otherwise when restoring state this hangs #: startup self.path = QtSvgDoc(sys.stdin, **self.document_kwargs) elif self.document and os.path.exists(self.document): self.path = QtSvgDoc(self.document, **self.document_kwargs) def _create_copy(self): """ Creates a copy of the original graphic applying the given transforms """ bbox = self.path.boundingRect() # Create the base copy t = QtGui.QTransform() t.scale( self.scale[0] * (self.mirror[0] and -1 or 1), self.scale[1] * (self.mirror[1] and -1 or 1), ) # Rotate about center if self.rotation != 0: c = bbox.center() t.translate(-c.x(), -c.y()) t.rotate(self.rotation) t.translate(c.x(), c.y()) # Apply transform path = self.path * t # Add weedline to copy if self.copy_weedline: self._add_weedline(path, self.copy_weedline_padding) # Apply ordering to path # this delegates to objects in the ordering module # TODO: Should this be done via plugins? OrderingHandler = ordering.REGISTRY.get(self.order) if OrderingHandler: path = OrderingHandler().order(self, path) # If it's too big we have to scale it w, h = path.boundingRect().width(), path.boundingRect().height() available_area = self.material.available_area #: This screws stuff up! if w > available_area.width() or h > available_area.height(): # If it's too big an auto scale is enabled, resize it to fit if not self.auto_scale: raise JobError("Image is too large to fit on the material") sx, sy = 1, 1 if w > available_area.width(): sx = available_area.width() / w if h > available_area.height(): sy = available_area.height() / h s = min(sx, sy) # Fit to the smaller of the two path = self.path * QtGui.QTransform.fromScale(s, s) # Move to bottom left p = path.boundingRect().bottomRight() path = path * QtGui.QTransform.fromTranslate(-p.x(), -p.y()) return path @contextmanager def events_suppressed(self): """ Block change events to prevent feedback loops """ self._blocked = True try: yield finally: self._blocked = False @observe('path', 'scale', 'auto_scale', 'lock_scale', 'mirror', 'align_center', 'rotation', 'auto_rotate', 'copies', 'order', 'copy_spacing', 'copy_weedline', 'copy_weedline_padding', 'plot_weedline', 'plot_weedline_padding', 'feed_to_end', 'feed_after', 'material', 'material.size', 'material.padding', 'auto_copies') def _job_changed(self, change): """ Recreate an instance of of the plot using the current settings """ if self._blocked: return if change['name'] == 'copies': self._desired_copies = self.copies #try: model = QtGui.QPainterPath() if not self.path: return path = self._create_copy() # Update size bbox = path.boundingRect() self.size = [bbox.width(), bbox.height()] # Create copies c = 0 points = self._copy_positions_iter(path) if self.auto_copies: self.stack_size = self._compute_stack_sizes(path) if self.stack_size[0]: copies_left = self.copies % self.stack_size[0] if copies_left: # not a full stack with self.events_suppressed(): self.copies = self._desired_copies self.add_stack() while c < self.copies: x, y = next(points) model.addPath(path * QtGui.QTransform.fromTranslate(x, -y)) c += 1 # Create weedline if self.plot_weedline: self._add_weedline(model, self.plot_weedline_padding) # Move to 0,0 bbox = model.boundingRect() p = bbox.bottomLeft() tx, ty = -p.x(), -p.y() # Center or set to padding tx += ((self.material.width() - bbox.width()) / 2.0 if self.align_center[0] else self.material.padding_left) ty += (-(self.material.height() - bbox.height()) / 2.0 if self.align_center[1] else -self.material.padding_bottom) t = QtGui.QTransform.fromTranslate(tx, ty) model = model * t end_point = (QtCore.QPointF( 0, -self.feed_after + model.boundingRect().top()) if self.feed_to_end else QtCore.QPointF(0, 0)) model.moveTo(end_point) # Set new model self.model = model #.simplified() # Set device model #self.device_model = self.device.driver.prepare_job(self) #except: # # Undo the change # if 'oldvalue' in change: # setattr(change['object'],change['name'],change['oldvalue']) # raise #if not self.check_bounds(self.boundingRect(),self.available_area): # raise JobError( # "Plot outside of plotting area, increase the area" # "or decrease the scale or decrease number of copies!") def _check_bounds(self, plot, area): """ Checks that the width and height of plot are less than the width and height of area """ return plot.width() > area.width() or plot.height() > area.height() def _copy_positions_iter(self, path, axis=0): """ Generator that creates positions of points """ other_axis = axis + 1 % 2 p = [0, 0] bbox = path.boundingRect() d = (bbox.width(), bbox.height()) pad = self.copy_spacing stack_size = self._compute_stack_sizes(path) while True: p[axis] = 0 yield p # Beginning of each row for i in range(stack_size[axis] - 1): p[axis] += d[axis] + pad[axis] yield p p[other_axis] += d[other_axis] + pad[other_axis] def _compute_stack_sizes(self, path): # Usable area material = self.material a = [material.width(), material.height()] a[0] -= material.padding[Padding.LEFT] + material.padding[ Padding.RIGHT] a[1] -= material.padding[Padding.TOP] + material.padding[ Padding.BOTTOM] # Clone includes weedline but not spacing bbox = path.boundingRect() size = [bbox.width(), bbox.height()] stack_size = [0, 0] p = [0, 0] for i in range(2): # Compute stack while (p[i] + size[i]) < a[i]: # while another one fits stack_size[i] += 1 p[i] += size[i] + self.copy_spacing[i] # Add only to end self.stack_size = stack_size return stack_size def _add_weedline(self, path, padding): """ Adds a weedline to the path by creating a box around the path with the given padding """ bbox = path.boundingRect() w, h = bbox.width(), bbox.height() tl = bbox.topLeft() x = tl.x() - padding[Padding.LEFT] y = tl.y() - padding[Padding.TOP] w += padding[Padding.LEFT] + padding[Padding.RIGHT] h += padding[Padding.TOP] + padding[Padding.BOTTOM] path.addRect(x, y, w, h) return path @property def state(self): pass @property def move_path(self): """ Returns the path the head moves when not cutting """ # Compute the negative path = QtGui.QPainterPath() for i in range(self.model.elementCount()): e = self.model.elementAt(i) if e.isMoveTo(): path.lineTo(e.x, e.y) else: path.moveTo(e.x, e.y) return path @property def cut_path(self): """ Returns path where it is cutting """ return self.model # def get_offset_path(self,device): # """ Returns path where it is cutting """ # path = QtGui.QPainterPath() # _p = QtCore.QPointF(0,0) # previous point # step = 0.1 # for subpath in QtSvgDoc.toSubpathList(self.model):#.toSubpathPolygons(): # e = subpath.elementAt(0) # path.moveTo(QtCore.QPointF(e.x,e.y)) # length = subpath.length() # distance = 0 # while distance<=length: # t = subpath.percentAtLength(distance) # p = subpath.pointAtPercent(t) # a = subpath.angleAtPercent(t)+90 # #path.moveTo(p)#QtCore.QPointF(x,y)) # # TOOD: Do i need numpy here??? # x = p.x()+np.multiply(self.device.blade_offset,np.sin(np.deg2rad(a))) # y = p.y()+np.multiply(self.device.blade_offset,np.cos(np.deg2rad(a))) # path.lineTo(QtCore.QPointF(x,y)) # distance+=step # #_p = p # update last # # return path def add_stack(self): """ Add a complete stack or fill the row """ copies_left = self.stack_size[0] - (self.copies % self.stack_size[0]) if copies_left == 0: # Add full stack self.copies = self.copies + self.stack_size[0] else: # Fill stack self.copies = self.copies + copies_left def remove_stack(self): """ Remove a complete stack or the rest of the row """ if self.copies <= self.stack_size[0]: self.copies = 1 return copies_left = self.copies % self.stack_size[0] if copies_left == 0: # Add full stack self.copies = self.copies - self.stack_size[0] else: # Fill stack self.copies = self.copies - copies_left def clone(self): """ Return a cloned instance of this object """ state = self.__getstate__() state.update({ 'material': Material(**self.material.__getstate__()), 'info': JobInfo(**self.info.__getstate__()), }) return Job(**state)
1: 2 }], [{ 1: 2 }], [{ 2: '' }]), (Dict(int, int), [{ 1: 2 }], [{ 1: 2 }], [{ '': 2 }, { 2: '' }]), (Instance((int, float)), [1, 2.0], [1, 2.0], ['']), (ForwardInstance(lambda: (int, float)), [1, 2.0], [1, 2.0], ['']), (Typed(float), [1.0], [1.0], [1]), (ForwardTyped(lambda: float), [1.0], [1.0], [1]), (Subclass(CAtom), [Atom], [Atom], [int]), (ForwardSubclass(lambda: CAtom), [Atom], [Atom], [int]), ] + ([(Range(sys.maxsize, sys.maxsize + 2), [sys.maxsize, sys.maxsize + 2], [sys.maxsize, sys.maxsize + 2], [sys.maxsize - 1, sys.maxsize + 3])] if sys.version_info > (3, ) else [])) def test_validation_modes(member, set_values, values, raising_values): """Test the validation modes. """ class MemberTest(Atom):
class BridgedApplication(Application): """An abstract implementation of an Enaml application. This serves as a base class for both Android and iOS applications and provides support for the python event loop, the development server and the bridge. """ __id__ = Int(-1) #: View to display within the activity activity = Instance(Activity) #: If true, debug bridge statements debug = Bool() #: Use dev server dev = Str() _dev_session = Value() #: Event loop loop = Instance(IOLoop, factory=IOLoop.current) #: Events to send to the bridge _bridge_queue = List() #: Time last sent _bridge_max_delay = Float(0.005) #: Time last sent _bridge_last_scheduled = Float() #: Entry points to load plugins plugins = Dict() #: Event triggered when an error occurs error_occurred = Event(Exception) # ------------------------------------------------------------------------- # Defaults # ------------------------------------------------------------------------- def _default_plugins(self): """Get entry points to load any plugins installed. The build process should create an "entry_points.json" file with all of the data from the installed entry points. """ plugins = {} try: with open("entry_points.json") as f: entry_points = json.load(f) for ep, obj in entry_points.items(): plugins[ep] = [] for name, src in obj.items(): plugins[ep].append(Plugin(name=name, source=src)) except Exception as e: print(f"Failed to load entry points {e}") return plugins # ------------------------------------------------------------------------- # BridgedApplication Constructor # ------------------------------------------------------------------------- def __init__(self, *args, **kwargs): """Initialize the event loop error handler. Subclasses must properly initialize the proxy resolver. """ super().__init__(*args, **kwargs) if self.dev: self.start_dev_session() self.init_error_handler() self.load_plugin_widgets() self.load_plugin_factories() # ------------------------------------------------------------------------- # Abstract API Implementation # ------------------------------------------------------------------------- def start(self): """Start the application event loop""" #: Schedule a load view if given and remote debugging is not active #: the remote debugging init call this after dev connection is ready if self.dev != "remote": self.deferred_call(self.activity.start) self.loop.start() def stop(self): """Stop the application's main event loop.""" self.loop.stop() def deferred_call(self, callback, *args, **kwargs): """Invoke a callable on the next cycle of the main event loop thread. Parameters ---------- callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ self.loop.add_callback(callback, *args, **kwargs) def timed_call(self, ms, callback, *args, **kwargs): """Invoke a callable on the main event loop thread at a specified time in the future. Parameters ---------- ms : int The time to delay, in milliseconds, before executing the callable. callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ self.loop.call_later(ms / 1000, callback, *args, **kwargs) def is_main_thread(self): """Indicates whether the caller is on the main gui thread. Returns ------- result : bool True if called from the main gui thread. False otherwise. """ return False # ------------------------------------------------------------------------- # App API Implementation # ------------------------------------------------------------------------- async def has_permission(self, permission): """Return a future that resolves with the result of the permission""" raise NotImplementedError async def request_permissions(self, permissions): """Return a future that resolves with the result of the permission request """ raise NotImplementedError # ------------------------------------------------------------------------- # EventLoop API Implementation # ------------------------------------------------------------------------- def init_error_handler(self): """When an error occurs, set the error view in the App""" # HACK: Reassign the discard method to show errors self.loop._discard_future_result = self._on_future_result def create_future(self, return_type: Optional[type] = None) -> BridgeFuture: """Create a future object using the EventLoop implementation""" return BridgeFuture(return_type) # ------------------------------------------------------------------------- # Bridge API Implementation # ------------------------------------------------------------------------- def show_error(self, msg: Union[bytes, str]): """Show the error view with the given message on the UI.""" self.send_event(Command.ERROR, msg) def send_event(self, name: str, *args, **kwargs): """Send an event to the native handler. This call is queued and batched. Parameters ---------- name : str The event name to be processed by MainActivity.processMessages. *args: args The arguments required by the event. **kwargs: kwargs Options for sending. These are: now: boolean Send the event now """ n = len(self._bridge_queue) # Add to queue self._bridge_queue.append((name, args)) if n == 0: # First event, send at next available time self._bridge_last_scheduled = time() self.deferred_call(self._bridge_send) return elif kwargs.get("now"): self._bridge_send(now=True) return # If it's been over 5 ms since we last scheduled, run now dt = time() - self._bridge_last_scheduled if dt > self._bridge_max_delay: self._bridge_send(now=True) def force_update(self): """Force an update now.""" #: So we don't get out of order self._bridge_send(now=True) def _on_future_result(self, future: Future) -> None: """Avoid unhandled-exception warnings from spawned coroutines.""" try: future.result() except Exception as e: self.handle_error(future, e) def _bridge_send(self, now: bool = False): """Send the events over the bridge to be processed by the native handler. Parameters ---------- now: boolean Send all pending events now instead of waiting for deferred calls to finish. Use this when you want to update the screen """ if len(self._bridge_queue): if self.debug: print("======== Py --> Native ======") for i, event in enumerate(self._bridge_queue): print(f"{i}: {event}") print("===========================") self.dispatch_events(dumps(self._bridge_queue)) self._bridge_queue = [] def dispatch_events(self, data): """Send events to the bridge using the system specific implementation.""" raise NotImplementedError async def process_events(self, data: str): """The native implementation must use this call to""" events = loads(data) if self.debug: print("======== Py <-- Native ======") for event in events: print(event) print("===========================") for t, event in events: if t == "event": await self.handle_event(*event) async def handle_event(self, result_id: int, ptr: int, method: str, args: list): """When we get an 'event' type from the bridge handle it by invoking the handler and if needed sending back the result. """ obj = None result = None try: obj, handler = get_handler(ptr, method) if method == "set_exception": # Remote call failed obj.set_exception(BridgeException(args)) elif iscoroutinefunction(handler): result = await handler(*(v for t, v in args)) else: result = handler(*(v for t, v in args)) except BridgeReferenceError as e: #: Log the event, don't blow up here event = (result_id, ptr, method, args) print(f"Error processing event: {event} - {e}") self.error_occurred(e) # type: ignore # self.show_error(msg) except Exception as e: #: Log the event, blow up in user's face self.error_occurred(e) # type: ignore err = traceback.format_exc() event = (result_id, ptr, method, args) msg = f"Error processing event: {event} - {err}" print(msg) self.show_error(msg) raise finally: if result_id: if hasattr(obj, "__nativeclass__"): sig, ret_type = getattr(obj.__class__, method).__returns__ else: sig = result.__class__.__name__ self.send_event( Command.RESULT, #: method result_id, (sig, encode(result)), #: args now=True, ) def handle_error(self, callback, exc: Exception): """Called when an error occurs in an event loop callback. By default, sets the error view. """ self.error_occurred(exc) # type: ignore msg = f"Exception in callback {callback}: {traceback.format_exc()}" self.show_error(msg.encode("utf-8")) # ------------------------------------------------------------------------- # AppEventListener API Implementation # ------------------------------------------------------------------------- def on_events(self, data): """Called when the bridge sends an event. For instance the return result of a method call or a callback from a widget event. """ #: Pass to event loop thread self.deferred_call(self.process_events, data) def on_pause(self): """Called when the app is paused.""" self.activity.paused() def on_resume(self): """Called when the app is resumed.""" self.activity.resumed() def on_stop(self): """Called when the app is stopped.""" #: Called from thread, make sure the correct thread detaches self.activity.stopped() def on_destroy(self): """Called when the app is destroyed.""" self.deferred_call(self.stop) # ------------------------------------------------------------------------- # Dev Session Implementation # ------------------------------------------------------------------------- def start_dev_session(self): """Start a client that attempts to connect to the dev server running on the host `app.dev` """ try: from .dev import DevServerSession dev = self.dev if ":" in dev: host, port = dev.split(":") params = {"host": host, "port": int(port)} else: params = {"host": dev} session = DevServerSession.initialize(**params) session.start() #: Save a reference self._dev_session = session except Exception: self.show_error(traceback.format_exc()) # ------------------------------------------------------------------------- # Plugin implementation # ------------------------------------------------------------------------- def get_plugins(self, group: str) -> list[Plugin]: """Was going to use entry points but that requires a ton of stuff which will be extremely slow. """ return self.plugins.get(group, []) def load_plugin_widgets(self): """Pull widgets added via plugins using the `enaml_native_widgets` entry point. The entry point function must return a dictionary of Widget declarations to add to the core api. def install(): from charts.widgets.chart_view import BarChart, LineChart return { 'BarChart': BarChart, 'LineCart': LineChart, } """ from enamlnative.widgets import api for plugin in self.get_plugins(group="enaml_native_widgets"): get_widgets = plugin.load() for name, widget in iter(get_widgets()): #: Update the core api with these widgets setattr(api, name, widget) def load_plugin_factories(self): """Pull widgets added via plugins using the `enaml_native_ios_factories` or `enaml_native_android_factories` entry points. The entry point function must return a dictionary of Widget declarations to add to the factories for this platform. def install(): return { 'MyWidget':my_widget_factory, # etc... } """ raise NotImplementedError
class SharedDict(Atom): """ Dict wrapper using a lock to protect access to its values. Parameters ---------- default : callable, optional Callable to use as argument for defaultdict, if unspecified a regular dict is used. """ def __init__(self, default=None): super(SharedDict, self).__init__() if default is not None: self._dict = defaultdict(default) else: self._dict = {} @contextmanager def safe_access(self, key): """Context manager to safely manipulate a value of the dict. """ lock = self._lock lock.acquire() yield self._dict[key] lock.release() @contextmanager def locked(self): """Acquire the instance lock. """ self._lock.acquire() yield self self._lock.release() def get(self, key, default=None): with self._lock: aux = self._dict.get(key, default) return aux def items(self): with self.locked(): return self._dict.items() # ========================================================================= # --- Private API --------------------------------------------------------- # ========================================================================= #: Underlying dict. _dict = Instance((dict, defaultdict)) #: Re-entrant lock use to secure the access to the dict. _lock = Value(factory=RLock) def __getitem__(self, key): with self.locked(): aux = self._dict[key] return aux def __setitem__(self, key, value): with self._lock: self._dict[key] = value def __delitem__(self, key): with self._lock: del self._dict[key] def __contains__(self, key): return key in self._dict def __iter__(self): return iter(self._dict) def __len__(self): return len(self._dict)
class Job(Model): """ Create a plot depending on the properties set. Any property that is a traitlet will cause an update when the value is changed. """ #: Material this job will be run on material = Instance(Material, ()).tag(config=True) #: Path to svg document this job parses document = Str().tag(config=True) #: Nodes to restrict document_kwargs = Dict().tag(config=True) #: Meta info a the job info = Instance(JobInfo, ()).tag(config=True) # Job properties used for generating the plot size = ContainerList(Float(), default=[1, 1]) scale = ContainerList(Float(), default=[1, 1]).tag(config=True) auto_scale = Bool(False).tag( config=True, help="automatically scale if it's too big for the area") lock_scale = Bool(True).tag( config=True, help="automatically scale if it's too big for the area") mirror = ContainerList(Bool(), default=[False, False]).tag(config=True) align_center = ContainerList(Bool(), default=[False, False]).tag(config=True) rotation = Float(0).tag(config=True) auto_rotate = Bool(False).tag( config=True, help="automatically rotate if it saves space") copies = Int(1).tag(config=True) auto_copies = Bool(False).tag(config=True, help="always use a full stack") copy_spacing = ContainerList(Float(), default=[10, 10]).tag(config=True) copy_weedline = Bool(False).tag(config=True) copy_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) plot_weedline = Bool(False).tag(config=True) plot_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) order = Enum(*sorted(ordering.REGISTRY.keys())).tag(config=True) def _default_order(self): return 'Normal' feed_to_end = Bool(False).tag(config=True) feed_after = Float(0).tag(config=True) stack_size = ContainerList(Int(), default=[0, 0]) #: Filters to cut only certain items filters = ContainerList(filters.JobFilter) #: Original path parsed from the source document path = Instance(QtSvgDoc) #: Path filtered by layers/colors and ordered according to the order optimized_path = Instance(QPainterPath) #: Finaly copy using all the applied job properties #: This is what is actually cut out model = Instance(QPainterPath) _blocked = Bool(False) # block change events _desired_copies = Int(1) # required for auto copies def __setstate__(self, *args, **kwargs): """ Ensure that when restoring from disk the material and info are not set to None. Ideally these would be defined as Typed but the material may be made extendable at some point. """ super(Job, self).__setstate__(*args, **kwargs) if not self.info: self.info = JobInfo() if not self.material: self.material = Material() def _observe_document(self, change): """ Read the document from stdin """ if change['type'] == 'update' and self.document == '-': #: Only load from stdin when explicitly changed to it (when doing #: open from the cli) otherwise when restoring state this hangs #: startup self.path = QtSvgDoc(sys.stdin, **self.document_kwargs) elif self.document and os.path.exists(self.document): self.path = QtSvgDoc(self.document, **self.document_kwargs) # Recreate available filters when the document changes self.filters = self._default_filters() def _default_filters(self): results = [] if not self.path: return results for Filter in filters.REGISTRY.values(): try: results.extend(Filter.get_filter_options(self, self.path)) except Exception as e: log.error("Failed loading filters for: %s" % Filter) log.exception(e) return results def _default_optimized_path(self): """ Filter parts of the documen based on the selected layers and colors """ doc = self.path for f in self.filters: # If the color/layer is NOT enabled, then remove that color/layer if not f.enabled: log.debug("Applying filter {}".format(f)) doc = f.apply_filter(self, doc) # Apply ordering to path # this delegates to objects in the ordering module OrderingHandler = ordering.REGISTRY.get(self.order) if OrderingHandler: doc = OrderingHandler().order(self, doc) return doc @observe('path', 'order', 'filters') def _update_optimized_path(self, change): """ Whenever the loaded file (and parsed SVG path) changes update it based on the filters from the job. """ self.optimized_path = self._default_optimized_path() def _create_copy(self): """ Creates a copy of the original graphic applying the given transforms """ optimized_path = self.optimized_path bbox = optimized_path.boundingRect() # Create the base copy t = QTransform() t.scale( self.scale[0] * (self.mirror[0] and -1 or 1), self.scale[1] * (self.mirror[1] and -1 or 1), ) # Rotate about center if self.rotation != 0: c = bbox.center() t.translate(-c.x(), -c.y()) t.rotate(self.rotation) t.translate(c.x(), c.y()) # Apply transform path = optimized_path * t # Add weedline to copy if self.copy_weedline: self._add_weedline(path, self.copy_weedline_padding) # If it's too big we have to scale it w, h = path.boundingRect().width(), path.boundingRect().height() available_area = self.material.available_area #: This screws stuff up! if w > available_area.width() or h > available_area.height(): # If it's too big an auto scale is enabled, resize it to fit if self.auto_scale: sx, sy = 1, 1 if w > available_area.width(): sx = available_area.width() / w if h > available_area.height(): sy = available_area.height() / h s = min(sx, sy) # Fit to the smaller of the two path = optimized_path * QTransform.fromScale(s, s) # Move to bottom left p = path.boundingRect().bottomRight() path = path * QTransform.fromTranslate(-p.x(), -p.y()) return path @contextmanager def events_suppressed(self): """ Block change events to prevent feedback loops """ self._blocked = True try: yield finally: self._blocked = False @observe('path', 'scale', 'auto_scale', 'lock_scale', 'mirror', 'align_center', 'rotation', 'auto_rotate', 'copies', 'order', 'copy_spacing', 'copy_weedline', 'copy_weedline_padding', 'plot_weedline', 'plot_weedline_padding', 'feed_to_end', 'feed_after', 'material', 'material.size', 'material.padding', 'auto_copies') def update_document(self, change=None): """ Recreate an instance of of the plot using the current settings """ if self._blocked: return if change: name = change['name'] if name == 'copies': self._desired_copies = self.copies elif name in ('layer', 'color'): self._update_optimized_path(change) model = self.create() if model: self.model = model def create(self, swap_xy=False, scale=None): """ Create a path model that is rotated and scaled """ model = QPainterPath() if not self.path: return path = self._create_copy() # Update size bbox = path.boundingRect() self.size = [bbox.width(), bbox.height()] # Create copies c = 0 points = self._copy_positions_iter(path) if self.auto_copies: self.stack_size = self._compute_stack_sizes(path) if self.stack_size[0]: copies_left = self.copies % self.stack_size[0] if copies_left: # not a full stack with self.events_suppressed(): self.copies = self._desired_copies self.add_stack() while c < self.copies: x, y = next(points) model.addPath(path * QTransform.fromTranslate(x, -y)) c += 1 # Create weedline if self.plot_weedline: self._add_weedline(model, self.plot_weedline_padding) # Determine padding bbox = model.boundingRect() if self.align_center[0]: px = (self.material.width() - bbox.width()) / 2.0 else: px = self.material.padding_left if self.align_center[1]: py = -(self.material.height() - bbox.height()) / 2.0 else: py = -self.material.padding_bottom # Scale and rotate if scale: model *= QTransform.fromScale(*scale) px, py = px * abs(scale[0]), py * abs(scale[1]) if swap_xy: t = QTransform() t.rotate(90) model *= t # Move to 0,0 bbox = model.boundingRect() p = bbox.bottomLeft() tx, ty = -p.x(), -p.y() # If swapped, make sure padding is still correct if swap_xy: px, py = -py, -px tx += px ty += py model = model * QTransform.fromTranslate(tx, ty) end_point = (QPointF(0, -self.feed_after + model.boundingRect().top()) if self.feed_to_end else QPointF(0, 0)) model.moveTo(end_point) return model def _check_bounds(self, plot, area): """ Checks that the width and height of plot are less than the width and height of area """ return plot.width() > area.width() or plot.height() > area.height() def _copy_positions_iter(self, path, axis=0): """ Generator that creates positions of points """ other_axis = axis + 1 % 2 p = [0, 0] bbox = path.boundingRect() d = (bbox.width(), bbox.height()) pad = self.copy_spacing stack_size = self._compute_stack_sizes(path) while True: p[axis] = 0 yield p # Beginning of each row for i in range(stack_size[axis] - 1): p[axis] += d[axis] + pad[axis] yield p p[other_axis] += d[other_axis] + pad[other_axis] def _compute_stack_sizes(self, path): # Usable area material = self.material a = [material.width(), material.height()] a[0] -= material.padding[Padding.LEFT] + material.padding[ Padding.RIGHT] a[1] -= material.padding[Padding.TOP] + material.padding[ Padding.BOTTOM] # Clone includes weedline but not spacing bbox = path.boundingRect() size = [bbox.width(), bbox.height()] stack_size = [0, 0] p = [0, 0] for i in range(2): # Compute stack while (p[i] + size[i]) < a[i]: # while another one fits stack_size[i] += 1 p[i] += size[i] + self.copy_spacing[i] # Add only to end self.stack_size = stack_size return stack_size def _add_weedline(self, path, padding): """ Adds a weedline to the path by creating a box around the path with the given padding """ bbox = path.boundingRect() w, h = bbox.width(), bbox.height() tl = bbox.topLeft() x = tl.x() - padding[Padding.LEFT] y = tl.y() - padding[Padding.TOP] w += padding[Padding.LEFT] + padding[Padding.RIGHT] h += padding[Padding.TOP] + padding[Padding.BOTTOM] path.addRect(x, y, w, h) return path @property def state(self): pass @property def move_path(self): """ Returns the path the head moves when not cutting """ # Compute the negative path = QPainterPath() for i in range(self.model.elementCount()): e = self.model.elementAt(i) if e.isMoveTo(): path.lineTo(e.x, e.y) else: path.moveTo(e.x, e.y) return path @property def cut_path(self): """ Returns path where it is cutting """ return self.model # def get_offset_path(self,device): # """ Returns path where it is cutting """ # path = QPainterPath() # _p = QPointF(0,0) # previous point # step = 0.1 # for subpath in QtSvgDoc.toSubpathList(self.model):#.toSubpathPolygons(): # e = subpath.elementAt(0) # path.moveTo(QPointF(e.x,e.y)) # length = subpath.length() # distance = 0 # while distance<=length: # t = subpath.percentAtLength(distance) # p = subpath.pointAtPercent(t) # a = subpath.angleAtPercent(t)+90 # #path.moveTo(p)#QPointF(x,y)) # # TOOD: Do i need numpy here??? # x = p.x()+np.multiply(self.device.blade_offset,np.sin(np.deg2rad(a))) # y = p.y()+np.multiply(self.device.blade_offset,np.cos(np.deg2rad(a))) # path.lineTo(QPointF(x,y)) # distance+=step # #_p = p # update last # # return path def add_stack(self): """ Add a complete stack or fill the row """ stack_size = self.stack_size[0] if stack_size == 0: self.copies += 1 return # Don't divide by 0 copies_left = stack_size - (self.copies % stack_size) if copies_left == 0: # Add full stack self.copies += stack_size else: # Fill stack self.copies += copies_left def remove_stack(self): """ Remove a complete stack or the rest of the row """ stack_size = self.stack_size[0] if stack_size == 0 or self.copies <= stack_size: self.copies = 1 return copies_left = self.copies % stack_size if copies_left == 0: # Add full stack self.copies -= stack_size else: # Fill stack self.copies -= copies_left def clone(self): """ Return a cloned instance of this object """ state = self.__getstate__() state.update({ 'material': Material(**self.material.__getstate__()), 'info': JobInfo(**self.info.__getstate__()), }) return Job(**state)
class Builder(JavaBridgeObject): """ Builds a notification. Notes ------ The constructor automatically sets the when field to System.currentTimeMillis() and the audio stream to the STREAM_DEFAULT. """ __nativeclass__ = set_default('%s.NotificationCompat$Builder' % package) __signature__ = set_default( ('android.content.Context', 'java.lang.String')) addAction = JavaMethod('%s.NotificationCompat$Action' % package) addAction_ = JavaMethod('android.R', 'java.lang.CharSequence', 'android.app.PendingIntent') addInvisibleAction = JavaMethod('%s.NotificationCompat$Action' % package) addInvisibleAction_ = JavaMethod('android.R', 'java.lang.CharSequence', 'android.app.PendingIntent') addPerson = JavaMethod('java.lang.String') build = JavaMethod(returns='android.app.Notification') setAutoCancel = JavaMethod('boolean') setBadgeIconType = JavaMethod('android.R') setCategory = JavaMethod('java.lang.String') setChannelId = JavaMethod('java.lang.String') setColor = JavaMethod('android.graphics.Color') setColorized = JavaMethod('boolean') setContent = JavaMethod('android.widget.RemoteViews') setContentInfo = JavaMethod('java.lang.CharSequence') setContentIntent = JavaMethod('android.app.PendingIntent') setContentText = JavaMethod('java.lang.CharSequence') setContentTitle = JavaMethod('java.lang.CharSequence') setCustomBigContentView = JavaMethod('android.widget.RemoteViews') setCustomContentView = JavaMethod('android.widget.RemoteViews') setCustomHeadsUpContentView = JavaMethod('android.widget.RemoteViews') setDefaults = JavaMethod('int') setDeleteIntent = JavaMethod('android.app.PendingIntent') setExtras = JavaMethod('android.os.Bundle') setFullScreenIntent = JavaMethod('android.app.PendingIntent', 'boolean') setGroup = JavaMethod('java.lang.String') setGroupAlertBehavior = JavaMethod('int') setGroupSummary = JavaMethod('boolean') setLargeIcon = JavaMethod('android.graphics.Bitmap') setLights = JavaMethod('android.graphics.Color', 'int', 'int') setLocalOnly = JavaMethod('boolean') setNumber = JavaMethod('int') setOngoing = JavaMethod('boolean') setOnlyAlertOnce = JavaMethod('boolean') setPriority = JavaMethod('int') setProgress = JavaMethod('int', 'int', 'boolean') setPublicVersion = JavaMethod('android.app.Notification') setRemoteInputHistory = JavaMethod('Landroid.app.Notification;[') setShortcutId = JavaMethod('java.lang.String') setShowWhen = JavaMethod('boolean') setSmallIcon = JavaMethod('android.R') setSmallIcon_ = JavaMethod('android.R', 'int') setSortKey = JavaMethod('java.lang.String') setSound = JavaMethod('android.net.Uri') setSound_ = JavaMethod('android.net.Uri', 'int') setStyle = JavaMethod('%s.NotificationCompat$Style' % package) setSubText = JavaMethod('java.lang.CharSequence') setTicker = JavaMethod('java.lang.CharSequence', 'android.widget.RemoteViews') setTicker_ = JavaMethod('java.lang.CharSequence') setTimeoutAfter = JavaMethod('long') setUsesChronometer = JavaMethod('boolean') setVibrate = JavaMethod('J[') setVisibility = JavaMethod('int') setWhen = JavaMethod('long') #: Glide Manager for loading bitmaps manager = Instance(RequestManager) #: BroadcastReceiver for handling actions _receivers = List(BroadcastReceiver) def _default_manager(self): app = AndroidApplication.instance() return RequestManager(__id__=Glide.with__(app)) def load_bitmap(self, src): r = RequestBuilder(__id__=self.manager.load(src)).asBitmap() return Bitmap(__id__=r.__id__) def update(self, title="", text="", info="", sub_text="", ticker="", importance=NotificationManager.IMPORTANCE_DEFAULT, group="", group_summary=None, group_alert_behavior=None, small_icon="@mipmap/ic_launcher", large_icon="", number=None, ongoing=None, local_only=None, style=None, color=None, colorized=None, sound=None, light_color="", light_on=500, light_off=500, show_when=None, show_stopwatch=False, show_progress=False, progress_max=100, progress_current=0, progress_indeterminate=False, auto_cancel=True, timeout_after=0, sort_key="", vibration_pattern=[], shortcut_id="", category="", badge_icon_type=None, only_alert_once=None, notification_options=None, actions=None): """ Creates and shows a notification. On android 8+ the channel must be created. Parameters ---------- badge_icon_type: Int Sets which icon to display as a badge for this notification. category: String Set the notification category. channel_id: String The id of the channel this notification is displayed on color: String A color string ex #F00 for red colorized: Bool Set whether this notification should be colorized. group: String Set this notification to be part of a group of notifications sharing the same key. group_summary: Bool Set this notification to be the group summary for a group of notifications. group_alert_behavior: Int Sets the group alert behavior for this notification. title: String Set the title (first row) of the notification, in a standard notification. text: String Set the text (second row) of the notification, in a standard notification. info: String Set the large text at the right-hand side of the notification. sub_text: String Set the third line of text in the platform notification template. ticker: String Sets the "ticker" text which is sent to accessibility services. importance: Int The importance or priority of the notification number: Int Set the large number at the right-hand side of the notification. ongoing: Bool Set whether this is an ongoing notification. local_only: Bool Set whether or not this notification is only relevant to the current device. style: String A style resource string small_icon: String A resource identifier @drawable/my_drawable large_icon: String A resource that glide can load (file:// or http://) url show_when: Bool Control whether the timestamp set with setWhen is shown in the content view. show_stopwatch: Bool Show the when field as a stopwatch. show_progress: Bool Show a progressbar progress_current: Int Current progress progress_max: Int Max progress progress_indeterminate: Bool Progress should be indeterminate light_color: String Set the argb value that you would like the LED on the device to blink light_on: Int Set the on duration of the LED light_off: Int Set the off duration of the LED auto_cancel: Bool Close automatically when tapped only_alert_once: Bool Set this flag if you would only like the sound, vibrate and ticker to be played if the notification is not already showing timeout_after: Long Specifies the time at which this notification should be canceled, if it is not already canceled. sort_key: String Set a sort key that orders this notification among other notifications from the same package. shortcut_id: String If this notification is duplicative of a Launcher shortcut, sets the id of the shortcut, in case the Launcher wants to hide the shortcut. sound: String Set the Uri of the sound to play. vibration_pattern: List of Long Set the vibration pattern to use. notification_options: Int Set the default notification options that will be used. actions: List A list of actions to handle Returns -------- result: Future A future that resolves with the builder of the notification that was created. References ---------- - https://developer.android.com/guide/topics/ui/notifiers/notifications.html - https://developer.android.com/training/notify-user/build-notification.html """ if title: self.setContentTitle(title) if importance: self.setPriority(importance) if small_icon: self.setSmallIcon(small_icon) if text: self.setContentText(text) if sub_text: self.setSubText(sub_text) if ticker: self.setTicker_(ticker) if info: self.setContentInfo(info) if style: self.setStyle(style) if color: self.setColor(color) if colorized is not None: self.setColorized(bool(colorized)) if number is not None: self.setNumber(int(number)) if ongoing is not None: self.setOngoing(bool(ongoing)) if only_alert_once is not None: self.setOnlyAlertOnce(bool(only_alert_once)) if local_only is not None: self.setLocalOnly(bool(local_only)) if auto_cancel: self.setAutoCancel(bool(auto_cancel)) if show_when is not None: # When is shown by default self.setShowWhen(bool(show_when)) if show_stopwatch: self.setUsesChronometer(bool(show_stopwatch)) if show_progress: self.setProgress(int(progress_max), int(progress_current), bool(progress_indeterminate)) if timeout_after: self.setTimeoutAfter(long(timeout_after)) if category: self.setCategory(category) if group: self.setGroup(group) if group_summary is not None: self.setGroupSummary(bool(group_summary)) if group_alert_behavior is not None: self.setGroupAlertBehavior(int(group_alert_behavior)) if shortcut_id: self.setShortcutId(shortcut_id) if large_icon: self.setLargeIcon(self.load_bitmap(large_icon)) if light_color: self.setLights(light_color, light_on, light_off) if sort_key: self.setSortKey(sort_key) if sound: self.setSound(Uri.parse(sound)) if vibration_pattern: self.setVibrate([long(i) for i in vibration_pattern]) if badge_icon_type is not None: self.setBadgeIconType(int(badge_icon_type)) if notification_options is not None: self.setDefaults(int(notification_options)) if actions is not None: app = AndroidApplication.instance() for (action_icon, action_text, action_callback) in actions: action_key = "com.codelv.enamlnative.Notify{}".format( self.__id__) intent = Intent() intent.setAction(action_key) receiver = BroadcastReceiver.for_action(action_key, action_callback, single_shot=False) self._receivers.append(receiver) self.addAction_( "@mipmap/ic_launcher", action_text, PendingIntent.getBroadcast(app, 0, intent, 0)) def show(self): """ Build and show this notification """ app = AndroidApplication.instance() f = app.create_future() # Build and show it def on_ready(mgr): if not mgr: f.set_result(None) return self.build().then(lambda r, mgr=mgr: on_built(r, mgr)) def on_built(__id__, mgr): notification = Notification(__id__=__id__) mgr.notify(self.__id__, notification) f.set_result(self) NotificationManager.get().then(on_ready) return f
class QtTreeView(QtAbstractItemView, ProxyTreeView): #: Tree widget widget = Typed(QTreeView) #: Root index index = Instance(QModelIndex,()) def create_widget(self): self.widget = QTreeView(self.parent_widget()) def init_widget(self): super(QtTreeView, self).init_widget() d = self.declaration self.set_show_root(d.show_root) def init_model(self): self.set_model(QAtomTreeModel(parent=self.widget)) #-------------------------------------------------------------------------- # Widget Setters #-------------------------------------------------------------------------- def set_show_root(self,show): self.widget.setRootIsDecorated(show) def set_cell_padding(self,padding): self.widget.setStyleSheet("QTreeView::item { padding: %ipx }"%padding); def set_horizontal_minimum_section_size(self,size): self.widget.header().setMinimumSectionSize(size) def set_horizontal_stretch(self,stretch): self.widget.header().setStretchLastSection(stretch) def set_horizontal_headers(self, headers): self.widget.header().model().layoutChanged.emit() def set_resize_mode(self,mode): self.widget.header().setResizeMode(RESIZE_MODES[mode]) def set_show_horizontal_header(self,show): header = self.widget.header() header.show() if show else header.hide() def set_model(self, model): super(QtTreeView, self).set_model(model) #-------------------------------------------------------------------------- # View refresh handlers #-------------------------------------------------------------------------- def _refresh_visible_column(self, value): self._pending_column_refreshes -=1 if self._pending_column_refreshes==0: d = self.declaration # TODO: What about parents??? try: d.visible_column = max(0,min(value,self.model.columnCount(self.index)-d.visible_columns)) except RuntimeError: pass def _refresh_visible_row(self, value): self._pending_row_refreshes -=1 if self._pending_row_refreshes==0: d = self.declaration try: d.visible_row = max(0,min(value,self.model.rowCount(self.index)-d.visible_rows)) except RuntimeError: pass
class PollIOLoop(IOLoop): """Base class for IOLoops built around a select-like function. For concrete implementations, see `tornado.platform.epoll.EPollIOLoop` (Linux), `tornado.platform.kqueue.KQueueIOLoop` (BSD and Mac), or `tornado.platform.select.SelectIOLoop` (all platforms). """ _impl = Value() time_func = Callable() _handlers = Dict() _events = Dict() _callbacks = Instance(collections.deque) _timeouts = List() _cancellations = Int() _running = Bool() _stopped = Bool() _closing = Bool() _thread_ident = Value() _pid = Int() _blocking_signal_threshold = Value() _timeout_counter = Value() _waker = Value(Waker) def initialize(self, impl, time_func=None, **kwargs): super(PollIOLoop, self).initialize(**kwargs) self._impl = impl if hasattr(self._impl, 'fileno'): set_close_exec(self._impl.fileno()) self.time_func = time_func or time.time #self._handlers = {} #self._events = {} self._callbacks = collections.deque() #self._timeouts = [] #self._cancellations = 0 #self._running = False #self._stopped = False #self._closing = False #self._thread_ident = None self._pid = os.getpid() #self._blocking_signal_threshold = None self._timeout_counter = itertools.count() # Create a pipe that we send bogus data to when we want to wake # the I/O loop when it is idle self._waker = Waker() self.add_handler(self._waker.fileno(), lambda fd, events: self._waker.consume(), self.READ) @classmethod def configurable_base(cls): return PollIOLoop @classmethod def configurable_default(cls): if hasattr(select, "epoll"): from .platforms import EPollIOLoop return EPollIOLoop if hasattr(select, "kqueue"): # Python 2.6+ on BSD or Mac from .platforms import KQueueIOLoop return KQueueIOLoop from .platforms import SelectIOLoop return SelectIOLoop def close(self, all_fds=False): self._closing = True self.remove_handler(self._waker.fileno()) if all_fds: for fd, handler in list(self._handlers.values()): self.close_fd(fd) self._waker.close() self._impl.close() self._callbacks = None self._timeouts = None def add_handler(self, fd, handler, events): fd, obj = self.split_fd(fd) self._handlers[fd] = (obj, stack_context.wrap(handler)) self._impl.register(fd, events | self.ERROR) def update_handler(self, fd, events): fd, obj = self.split_fd(fd) self._impl.modify(fd, events | self.ERROR) def remove_handler(self, fd): fd, obj = self.split_fd(fd) self._handlers.pop(fd, None) self._events.pop(fd, None) try: self._impl.unregister(fd) except Exception: gen_log.debug("Error deleting fd from IOLoop", exc_info=True) def set_blocking_signal_threshold(self, seconds, action): if not hasattr(signal, "setitimer"): gen_log.error( "set_blocking_signal_threshold requires a signal module " "with the setitimer method") return self._blocking_signal_threshold = seconds if seconds is not None: signal.signal(signal.SIGALRM, action if action is not None else signal.SIG_DFL) def start(self): if self._running: raise RuntimeError("IOLoop is already running") if os.getpid() != self._pid: raise RuntimeError("Cannot share PollIOLoops across processes") self._setup_logging() if self._stopped: self._stopped = False return old_current = getattr(IOLoop._current, "instance", None) IOLoop._current.instance = self self._thread_ident = thread.get_ident() self._running = True # signal.set_wakeup_fd closes a race condition in event loops: # a signal may arrive at the beginning of select/poll/etc # before it goes into its interruptible sleep, so the signal # will be consumed without waking the select. The solution is # for the (C, synchronous) signal handler to write to a pipe, # which will then be seen by select. # # In python's signal handling semantics, this only matters on the # main thread (fortunately, set_wakeup_fd only works on the main # thread and will raise a ValueError otherwise). # # If someone has already set a wakeup fd, we don't want to # disturb it. This is an issue for twisted, which does its # SIGCHLD processing in response to its own wakeup fd being # written to. As long as the wakeup fd is registered on the IOLoop, # the loop will still wake up and everything should work. old_wakeup_fd = None if hasattr(signal, 'set_wakeup_fd') and os.name == 'posix': # requires python 2.6+, unix. set_wakeup_fd exists but crashes # the python process on windows. try: old_wakeup_fd = signal.set_wakeup_fd( self._waker.write_fileno()) if old_wakeup_fd != -1: # Already set, restore previous value. This is a little racy, # but there's no clean get_wakeup_fd and in real use the # IOLoop is just started once at the beginning. signal.set_wakeup_fd(old_wakeup_fd) old_wakeup_fd = None except ValueError: # Non-main thread, or the previous value of wakeup_fd # is no longer valid. old_wakeup_fd = None try: while True: # Prevent IO event starvation by delaying new callbacks # to the next iteration of the event loop. ncallbacks = len(self._callbacks) # Add any timeouts that have come due to the callback list. # Do not run anything until we have determined which ones # are ready, so timeouts that call add_timeout cannot # schedule anything in this iteration. due_timeouts = [] if self._timeouts: now = self.time() while self._timeouts: if self._timeouts[0].callback is None: # The timeout was cancelled. Note that the # cancellation check is repeated below for timeouts # that are cancelled by another timeout or callback. heapq.heappop(self._timeouts) self._cancellations -= 1 elif self._timeouts[0].deadline <= now: due_timeouts.append(heapq.heappop(self._timeouts)) else: break if (self._cancellations > 512 and self._cancellations > (len(self._timeouts) >> 1)): # Clean up the timeout queue when it gets large and it's # more than half cancellations. self._cancellations = 0 self._timeouts = [ x for x in self._timeouts if x.callback is not None ] heapq.heapify(self._timeouts) for i in range(ncallbacks): self._run_callback(self._callbacks.popleft()) for timeout in due_timeouts: if timeout.callback is not None: self._run_callback(timeout.callback) # Closures may be holding on to a lot of memory, so allow # them to be freed before we go into our poll wait. due_timeouts = timeout = None if self._callbacks: # If any callbacks or timeouts called add_callback, # we don't want to wait in poll() before we run them. poll_timeout = 0.0 elif self._timeouts: # If there are any timeouts, schedule the first one. # Use self.time() instead of 'now' to account for time # spent running callbacks. poll_timeout = self._timeouts[0].deadline - self.time() poll_timeout = max(0, min(poll_timeout, _POLL_TIMEOUT)) else: # No timeouts and no callbacks, so use the default. poll_timeout = _POLL_TIMEOUT if not self._running: break if self._blocking_signal_threshold is not None: # clear alarm so it doesn't fire while poll is waiting for # events. signal.setitimer(signal.ITIMER_REAL, 0, 0) try: event_pairs = self._impl.poll(poll_timeout) except Exception as e: # Depending on python version and IOLoop implementation, # different exception types may be thrown and there are # two ways EINTR might be signaled: # * e.errno == errno.EINTR # * e.args is like (errno.EINTR, 'Interrupted system call') if errno_from_exception(e) == errno.EINTR: continue else: raise if self._blocking_signal_threshold is not None: signal.setitimer(signal.ITIMER_REAL, self._blocking_signal_threshold, 0) # Pop one fd at a time from the set of pending fds and run # its handler. Since that handler may perform actions on # other file descriptors, there may be reentrant calls to # this IOLoop that modify self._events self._events.update(event_pairs) while self._events: fd, events = self._events.popitem() try: fd_obj, handler_func = self._handlers[fd] handler_func(fd_obj, events) except (OSError, IOError) as e: if errno_from_exception(e) == errno.EPIPE: # Happens when the client closes the connection pass else: self.handle_callback_exception( self._handlers.get(fd)) except Exception: self.handle_callback_exception(self._handlers.get(fd)) fd_obj = handler_func = None finally: # reset the stopped flag so another start/stop pair can be issued self._stopped = False if self._blocking_signal_threshold is not None: signal.setitimer(signal.ITIMER_REAL, 0, 0) IOLoop._current.instance = old_current if old_wakeup_fd is not None: signal.set_wakeup_fd(old_wakeup_fd) def stop(self): self._running = False self._stopped = True self._waker.wake() def time(self): return self.time_func() def call_at(self, deadline, callback, *args, **kwargs): timeout = _Timeout( deadline, functools.partial(stack_context.wrap(callback), *args, **kwargs), self) heapq.heappush(self._timeouts, timeout) return timeout def remove_timeout(self, timeout): # Removing from a heap is complicated, so just leave the defunct # timeout object in the queue (see discussion in # http://docs.python.org/library/heapq.html). # If this turns out to be a problem, we could add a garbage # collection pass whenever there are too many dead timeouts. timeout.callback = None self._cancellations += 1 def add_callback(self, callback, *args, **kwargs): if self._closing: return # Blindly insert into self._callbacks. This is safe even # from signal handlers because deque.append is atomic. self._callbacks.append( functools.partial(stack_context.wrap(callback), *args, **kwargs)) if thread.get_ident() != self._thread_ident: # This will write one byte but Waker.consume() reads many # at once, so it's ok to write even when not strictly # necessary. self._waker.wake() else: # If we're on the IOLoop's thread, we don't need to wake anyone. pass def add_callback_from_signal(self, callback, *args, **kwargs): with stack_context.NullContext(): self.add_callback(callback, *args, **kwargs)
class Measure(Atom): """ Attributes ---------- is_running : bool Boolean indicating whether or not the measurement is running. root_task : instance(RootTask) `RootTask` instance representing the enqueued measure. monitor : instance(MeasureMonitor) Monitor which can be used to follow the measurement. use_monitor : bool Boolean indicating whether or not to use a monitor to follow the measure. """ is_running = Bool(False) root_task = Instance(RootTask, ()) monitor = Instance(MeasureMonitor, ()) use_monitor = Bool(True) def save_measure(self, path): """ """ config = ConfigObj(path, indent_type=' ') config['root_task'] = save_task(self.root_task, mode='config').dict() config['monitor'] = self.monitor.save_monitor_state() config.write() def load_measure(self, path): """ """ config = ConfigObj(path) if 'root_task' in config: self.root_task = build_root(mode='config', config=config['root_task']) self.monitor.load_monitor_state(config['monitor']) else: #Assume this a raw root_task file without name or monitor self.root_task = build_root(mode='config', config=config) def _observe_root_task(self, change): """ """ if 'oldvalue' in change: change['oldvalue'].unobserve('task_database.notifier', self.monitor.database_modified) self.monitor.clear_state() change['value'].task_database.observe('notifier', self.monitor.database_modified) database = change['value'].task_database self.monitor.database_entries = database.list_all_entries() self.monitor.refresh_monitored_entries() self.monitor.measure_name = '' def _observe_is_running(self, change): """ """ new = change['value'] if new: self.monitor.status = 'RUNNING'
class ProfileForm(Atom): """ Simple model representing the informations stored in an instrument profile. Parameters ---------- **kwargs Keyword arguments used to initialize attributes and form attributes Attributes ---------- manager: InstrManagerPlugin A reference to the current instrument manager. name : str Name of the instrument used to identify him. Should be different from the driver name driver_type : str Kind of driver to use, ie which kind of standard is used to communicate driver_list : list(Unicodestr) List of known driver matching `driver_type` driver : str Name of the selected driver connection_form : instance(AbstractConnectionForm) Form used to display the informations specific to the `driver_type` """ manager = Typed(InstrManagerPlugin) name = Unicode('') driver_type = Str('') drivers = List(Str(), []) driver = Str('') connection_form = Instance(AbstractConnectionForm) connection_form_view = Value() def __init__(self, **kwargs): super(ProfileForm, self).__init__() self.manager = kwargs.pop('manager') if 'name' in kwargs: self.name = kwargs.pop('name') if 'driver_type' in kwargs: self.driver_type = kwargs.pop('driver_type') if 'driver' in kwargs: self.driver = kwargs.pop('driver') if self.driver or self.driver_type: aux = self.driver if self.driver else self.driver_type form_class, view = self.manager.matching_form(aux, view=True) if form_class: if self.driver: aux, _ = self.manager.drivers_request([self.driver]) self.connection_form = form_class(driver=aux[self.driver], **kwargs) else: self.connection_form = form_class(**kwargs) def dict(self): """ Return the informations of the form as a dict """ infos = {'name': self.name, 'driver_type': self.driver_type, 'driver': self.driver} if self.connection_form: infos.update(self.connection_form.connection_dict()) return infos def _observe_driver_type(self, change): """Build the list of driver matching the selected type. """ new_type = change['value'] if new_type: driver_list = self.manager.matching_drivers([new_type]) self.drivers = sorted(driver_list) form_class, view = self.manager.matching_form(new_type, view=True) if form_class: self.connection_form = form_class() self.connection_form_view = view else: self.connection_form = None self.connection_form_view = None def _observe_driver(self, change): """ Select the right connection_form for the selected driver. """ driver = change['value'] if driver: form_class, view = self.manager.matching_form(driver, view=True) aux, _ = self.manager.drivers_request([driver]) if form_class: if isinstance(self.connection_form, form_class): self.connection_form.driver = aux[driver] else: self.connection_form = form_class(driver=driver) self.connection_form_view = view else: self.connection_form = None self.connection_form_view = None
class Tag(ToolkitObject): #: Reference to the proxy object proxy = Typed(ProxyTag) #: Object ID id = d_(Unicode()) #: Tag name tag = d_(Unicode()).tag(attr=False) #: CSS classes cls = d_(Instance((list, object))).tag(attr=False) #: CSS styles style = d_(Instance((dict, object))).tag(attr=False) #: Node text text = d_(Unicode()).tag(attr=False) #: Node tail text tail = d_(Unicode()).tag(attr=False) #: Alt attribute alt = d_(Unicode()) #: Custom attributes not explicitly defined attrs = d_(Dict()).tag(attr=False) #: JS onclick definition onclick = d_(Unicode()) #: Used to tell js to send click events back to the server clickable = d_(Coerced(bool)) #: Event triggered on click clicked = d_(Event()) #: Used to tell js to send drag events back to the server and sets the #: draggable attribute. Must be used with ondragstart. draggable = d_(Coerced(bool)).tag(attr=False) #: JS ondragstart definition ondragstart = d_(Unicode()) #: JS ondragover definition ondragover = d_(Unicode()) #: JS ondrop definition ondrop = d_(Unicode()) #: Event triggered when a drop occurs dropped = d_(Event(ToolkitObject)) def _default_id(self): return '%0x' % id(self) @observe('id', 'tag', 'cls', 'style', 'text', 'tail', 'alt', 'attrs', 'onclick', 'clickable', 'ondragstart', 'ondragover', 'ondrop', 'draggable') def _update_proxy(self, change): """ Update the proxy widget when the Widget data changes. This also notifies the root that the dom has been modified. """ #: Try default handler t = change['type'] if t == 'update' and self.proxy_is_active: name = change['name'] value = change['value'] handler = getattr(self.proxy, 'set_' + name, None) if handler is not None: handler(value) else: self.proxy.set_attribute(name, value) self._notify_modified({ 'id': self.id, 'type': t, 'name': name, 'value': value }) def _notify_modified(self, change): """ Triggers a modified event on the root node with the given change. Parameters ---------- change: Dict A change event dict indicating what change has occurred. """ root = self.root_object() if isinstance(root, Html): root.modified(change) # ========================================================================= # Object API # ========================================================================= def child_added(self, child): super(Tag, self).child_added(child) if isinstance(child, Tag) and self.proxy_is_active: change = { 'id': self.id, 'type': 'added', 'name': 'children', 'value': child.render() } # Indicate where it was added children = self.children i = children.index(child) + 1 while i < len(children): c = children[i] if isinstance(c, Tag): # Ignore pattern nodes change['before'] = c.id break else: i += 1 # else added to the end self._notify_modified(change) def child_moved(self, child): super(Tag, self).child_moved(child) if isinstance(child, Tag) and self.proxy_is_active: if self.proxy.child_moved(child.proxy): change = { 'id': self.id, 'type': 'moved', 'name': 'children', 'value': child.id } # Indicate where it was moved to children = self.children i = children.index(child) + 1 while i < len(children): c = children[i] if isinstance(c, Tag): # Ignore pattern nodes change['before'] = c.id break else: i += 1 # else moved to the end self._notify_modified(change) def child_removed(self, child): """ Handles the child removed event. This will generate a modified event indicating which child was removed. """ super(Tag, self).child_removed(child) if isinstance(child, Tag) and self.proxy_is_active: self._notify_modified({ 'id': self.id, 'type': 'removed', 'name': 'children', 'value': child.id, }) # ========================================================================= # Tag API # ========================================================================= def xpath(self, query, **kwargs): """ Find nodes matching the given xpath query Parameters ---------- query: String The xpath query to run Returns ------- nodes: List[Tag] List of tags matching the xpath query. """ nodes = self.proxy.xpath(query, **kwargs) return [n.declaration for n in nodes] def prepare(self, **kwargs): """ Prepare this node for rendering. This sets any attributes given, initializes and actives the proxy as needed. """ for k, v in kwargs.items(): setattr(self, k, v) if not self.is_initialized: self.initialize() if not self.proxy_is_active: self.activate_proxy() def render(self, **kwargs): """ Render this tag and all children to a string. Returns ------- html: String The rendered html content of the node. """ self.prepare(**kwargs) return self.proxy.render()
class DevServerSession(Atom): """ Connect to a dev server running on the LAN or if host is 0.0.0.0 server a page to let code be pasted in. Note this should NEVER be used in a released app! """ #: Singleton Instance of this class _instance = None #: Reference to the current Application app = ForwardInstance(get_app) #: Host to connect to (in client mode) or #: if set to "server" it will enable "server" mode host = Unicode() #: Port to serve on (in server mode) or port to connect to (in client mode) port = Int(8888) #: URL to connect to (in client mode) url = Unicode('ws://192.168.21.119:8888/dev') #: Websocket connection state connected = Bool() #: Message buffer buf = Unicode() #: Dev session mode mode = Enum('client', 'server', 'remote') #: Hotswap support class hotswap = Instance(Hotswapper) #: Delegate dev server servers = List(Subclass(DevServer), default=[ TornadoDevServer, TwistedDevServer, ]) server = Instance(DevServer) #: Delegate dev client clients = List(Subclass(DevClient), default=[ TornadoDevClient, TwistedDevClient, ]) client = Instance(DevClient) # ------------------------------------------------------------------------- # Initialization # ------------------------------------------------------------------------- @classmethod def initialize(cls, *args, **kwargs): """ Create an instance of this class. """ try: return DevServerSession(*args, **kwargs) except ImportError: pass @classmethod def instance(cls): """ Get the singleton instance """ return cls._instance def __init__(self, *args, **kwargs): """ Overridden constructor that forces only one instance to ever exist. """ if self.instance() is not None: raise RuntimeError("A DevServerClient instance already exists!") super(DevServerSession, self).__init__(*args, **kwargs) DevServerSession._instance = self def start(self): """ Start the dev session. Attempt to use tornado first, then try twisted """ print("Starting debug client cwd: {}".format(os.getcwd())) print("Sys path: {}".format(sys.path)) #: Initialize the hotswapper self.hotswap = Hotswapper(debug=False) if self.mode == 'server': self.server.start(self) else: self.client.start(self) # ------------------------------------------------------------------------- # Defaults # ------------------------------------------------------------------------- def _default_mode(self): """ If host is set to server then serve it from the app! """ host = self.host if host == 'server': return 'server' elif host == 'remote': return 'remote' return 'client' def _default_url(self): """ Websocket URL to connect to and listen for reload requests """ host = 'localhost' if self.mode == 'remote' else self.host return 'ws://{}:{}/dev'.format(host, self.port) def _default_app(self): """ Application instance """ return get_app().instance() def _default_server(self): for Server in self.servers: if Server.available(): return Server() raise NotImplementedError( "No dev servers are available! " "Include tornado or twisted in your requirements!") def _default_client(self): for Client in self.clients: if Client.available(): return Client() raise NotImplementedError( "No dev clients are available! " "Include tornado or twisted in your requirements!") def _observe_connected(self, change): """ Log connection state changes """ print("Dev session {}".format( "connected" if self.connected else "disconnected")) # ------------------------------------------------------------------------- # Dev Session API # ------------------------------------------------------------------------- def write_message(self, data, binary=False): """ Write a message to the active client """ self.client.write_message(data, binary=binary) def handle_message(self, data): """ When we get a message """ msg = json.loads(data) print("Dev server message: {}".format(msg)) handler_name = 'do_{}'.format(msg['type']) if hasattr(self, handler_name): handler = getattr(self, handler_name) result = handler(msg) return {'ok': True, 'result': result} else: err = "Warning: Unhandled message: {}".format(msg) print(err) return {'ok': False, 'message': err} # ------------------------------------------------------------------------- # Message handling API # ------------------------------------------------------------------------- def do_reload(self, msg): """ Called when the dev server wants to reload the view. """ #: TODO: This should use the autorelaoder app = self.app #: Show loading screen try: self.app.widget.showLoading("Reloading... Please wait.", now=True) #self.app.widget.restartPython(now=True) #sys.exit(0) except: #: TODO: Implement for iOS... pass self.save_changed_files(msg) if app.load_view is None: print("Warning: Reloading the view is not implemented. " "Please set `app.load_view` to support this.") return if app.view is not None: try: app.view.destroy() except: pass def wrapped(f): def safe_reload(*args, **kwargs): try: return f(*args, **kwargs) except: #: Display the error app.send_event(Command.ERROR, traceback.format_exc()) return safe_reload app.deferred_call(wrapped(app.load_view), app) def do_hotswap(self, msg): """ Attempt to hotswap the code """ #: Show hotswap tooltip try: self.app.widget.showTooltip("Hot swapping...", now=True) except: pass self.save_changed_files(msg) hotswap = self.hotswap app = self.app try: print("Attempting hotswap....") with hotswap.active(): hotswap.update(app.view) except: #: Display the error app.send_event(Command.ERROR, traceback.format_exc()) # ------------------------------------------------------------------------- # Utility methods # ------------------------------------------------------------------------- def save_changed_files(self, msg): #: On iOS we can't write in the app bundle if os.environ.get('TMP'): tmp_dir = os.environ['TMP'] if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) if tmp_dir not in sys.path: sys.path.insert(0, tmp_dir) import site reload(site) with cd(sys.path[0]): #: Clear cache if os.path.exists('__enamlcache__'): shutil.rmtree('__enamlcache__') for fn in msg['files']: print("Updating {}".format(fn)) folder = os.path.dirname(fn) if folder and not os.path.exists(folder): os.makedirs(folder) with open(fn, 'wb') as f: f.write(msg['files'][fn].encode('utf-8'))
class BridgedApplication(Application): """ An abstract implementation of an Enaml application. This serves as a base class for both Android and iOS applications and provides support for the python event loop, the development server and the bridge. """ __id__ = Int(-1) #: Keep screen on by setting the WindowManager flag keep_screen_on = Bool() #: Application lifecycle state must be set by the implementation state = Enum('created', 'paused', 'resumed', 'stopped', 'destroyed') #: View to display within the activity view = Value() #: Factory to create and show the view. It takes the app as the first arg load_view = Callable() #: If true, debug bridge statements debug = Bool() #: Use dev server dev = Unicode() _dev_session = Value() #: Event loop loop = Instance(EventLoop) #: Events to send to the bridge _bridge_queue = List() #: Time last sent _bridge_max_delay = Float(0.005) #: Time last sent _bridge_last_scheduled = Float() #: Entry points to load plugins plugins = Dict() # ------------------------------------------------------------------------- # Defaults # ------------------------------------------------------------------------- def _default_loop(self): """ Get the event loop based on what libraries are available. """ return EventLoop.default() def _default_plugins(self): """ Get entry points to load any plugins installed. The build process should create an "entry_points.json" file with all of the data from the installed entry points. """ plugins = {} try: with open('entry_points.json') as f: entry_points = json.load(f) for ep, obj in entry_points.items(): plugins[ep] = [] for name, src in obj.items(): plugins[ep].append(Plugin(name=name, source=src)) except Exception as e: print("Failed to load entry points {}".format(e)) return plugins # ------------------------------------------------------------------------- # BridgedApplication Constructor # ------------------------------------------------------------------------- def __init__(self, *args, **kwargs): """ Initialize the event loop error handler. Subclasses must properly initialize the proxy resolver. """ super(BridgedApplication, self).__init__(*args, **kwargs) if self.dev: self.start_dev_session() self.init_error_handler() self.load_plugin_widgets() self.load_plugin_factories() # ------------------------------------------------------------------------- # Abstract API Implementation # ------------------------------------------------------------------------- def start(self): """ Start the application's main event loop using either twisted or tornado. """ #: Schedule a load view if given and remote debugging is not active #: the remote debugging init call this after dev connection is ready if self.load_view and self.dev != "remote": self.deferred_call(self.load_view, self) self.loop.start() def stop(self): """ Stop the application's main event loop. """ self.loop.stop() def deferred_call(self, callback, *args, **kwargs): """ Invoke a callable on the next cycle of the main event loop thread. Parameters ---------- callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ return self.loop.deferred_call(callback, *args, **kwargs) def timed_call(self, ms, callback, *args, **kwargs): """ Invoke a callable on the main event loop thread at a specified time in the future. Parameters ---------- ms : int The time to delay, in milliseconds, before executing the callable. callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ return self.loop.timed_call(ms, callback, *args, **kwargs) def is_main_thread(self): """ Indicates whether the caller is on the main gui thread. Returns ------- result : bool True if called from the main gui thread. False otherwise. """ return False # ------------------------------------------------------------------------- # App API Implementation # ------------------------------------------------------------------------- def has_permission(self, permission): """ Return a future that resolves with the result of the permission """ raise NotImplementedError def request_permissions(self, permissions): """ Return a future that resolves with the result of the permission request """ raise NotImplementedError # ------------------------------------------------------------------------- # EventLoop API Implementation # ------------------------------------------------------------------------- def init_error_handler(self): """ When an error occurs, set the error view in the App """ self.loop.set_error_handler(self.handle_error) def create_future(self): """ Create a future object using the EventLoop implementation """ return self.loop.create_future() def run_iteration(self): """ Run an iteration of the event loop """ return self.loop.run_iteration() def add_done_callback(self, future, callback): """ Add a callback on a future object put here so it can be implemented with different event loops. Parameters ----------- future: Future or Deferred Future implementation for the current EventLoop callback: callable Callback to invoke when the future is done """ if future is None: raise bridge.BridgeReferenceError( "Tried to add a callback to a nonexistent Future. " "Make sure you pass the `returns` argument to your JavaMethod") return self.loop.add_done_callback(future, callback) def set_future_result(self, future, result): """ Set the result of the future Parameters ----------- future: Future or Deferred Future implementation for the current EventLoop result: object Result to set """ return self.loop.set_future_result(future, result) # ------------------------------------------------------------------------- # Bridge API Implementation # ------------------------------------------------------------------------- def show_view(self): """ Show the current `app.view`. This will fade out the previous with the new view. """ raise NotImplementedError def get_view(self): """ Get the root view to display. Make sure it is properly initialized. """ view = self.view if not view.is_initialized: view.initialize() if not view.proxy_is_active: view.activate_proxy() return view.proxy.widget def show_error(self, msg): """ Show the error view with the given message on the UI. """ self.send_event(bridge.Command.ERROR, msg) def send_event(self, name, *args, **kwargs): """ Send an event to the native handler. This call is queued and batched. Parameters ---------- name : str The event name to be processed by MainActivity.processMessages. *args: args The arguments required by the event. **kwargs: kwargs Options for sending. These are: now: boolean Send the event now """ n = len(self._bridge_queue) # Add to queue self._bridge_queue.append((name, args)) if n == 0: # First event, send at next available time self._bridge_last_scheduled = time() self.deferred_call(self._bridge_send) return elif kwargs.get('now'): self._bridge_send(now=True) return # If it's been over 5 ms since we last scheduled, run now dt = time() - self._bridge_last_scheduled if dt > self._bridge_max_delay: self._bridge_send(now=True) def force_update(self): """ Force an update now. """ #: So we don't get out of order self._bridge_send(now=True) def _bridge_send(self, now=False): """ Send the events over the bridge to be processed by the native handler. Parameters ---------- now: boolean Send all pending events now instead of waiting for deferred calls to finish. Use this when you want to update the screen """ if len(self._bridge_queue): if self.debug: print("======== Py --> Native ======") for event in self._bridge_queue: print(event) print("===========================") self.dispatch_events(bridge.dumps(self._bridge_queue)) self._bridge_queue = [] def dispatch_events(self, data): """ Send events to the bridge using the system specific implementation. """ raise NotImplementedError def process_events(self, data): """ The native implementation must use this call to """ events = bridge.loads(data) if self.debug: print("======== Py <-- Native ======") for event in events: print(event) print("===========================") for event in events: if event[0] == 'event': self.handle_event(event) def handle_event(self, event): """ When we get an 'event' type from the bridge handle it by invoking the handler and if needed sending back the result. """ result_id, ptr, method, args = event[1] obj = None result = None try: obj, handler = bridge.get_handler(ptr, method) result = handler(*[v for t, v in args]) except bridge.BridgeReferenceError as e: #: Log the event, don't blow up here msg = "Error processing event: {} - {}".format( event, e).encode("utf-8") print(msg) self.show_error(msg) except: #: Log the event, blow up in user's face msg = "Error processing event: {} - {}".format( event, traceback.format_exc()).encode("utf-8") print(msg) self.show_error(msg) raise finally: if result_id: if hasattr(obj, '__nativeclass__'): sig = getattr(type(obj), method).__returns__ else: sig = type(result).__name__ self.send_event( bridge.Command.RESULT, #: method result_id, bridge.msgpack_encoder(sig, result) #: args ) def handle_error(self, callback): """ Called when an error occurs in an event loop callback. By default, sets the error view. """ self.loop.log_error(callback) msg = "\n".join([ "Exception in callback %r"%callback, traceback.format_exc() ]) self.show_error(msg.encode('utf-8')) # ------------------------------------------------------------------------- # AppEventListener API Implementation # ------------------------------------------------------------------------- def on_events(self, data): """ Called when the bridge sends an event. For instance the return result of a method call or a callback from a widget event. """ #: Pass to event loop thread self.deferred_call(self.process_events, data) def on_pause(self): """ Called when the app is paused. """ pass def on_resume(self): """ Called when the app is resumed. """ pass def on_stop(self): """ Called when the app is stopped. """ #: Called from thread, make sure the correct thread detaches pass def on_destroy(self): """ Called when the app is destroyed. """ self.deferred_call(self.stop) # ------------------------------------------------------------------------- # Dev Session Implementation # ------------------------------------------------------------------------- def start_dev_session(self): """ Start a client that attempts to connect to the dev server running on the host `app.dev` """ try: from .dev import DevServerSession session = DevServerSession.initialize(host=self.dev) session.start() #: Save a reference self._dev_session = session except: self.show_error(traceback.format_exc()) # ------------------------------------------------------------------------- # Plugin implementation # ------------------------------------------------------------------------- def get_plugins(self, group): """ Was going to use entry points but that requires a ton of stuff which will be extremely slow. """ return self.plugins.get(group, []) def load_plugin_widgets(self): """ Pull widgets added via plugins using the `enaml_native_widgets` entry point. The entry point function must return a dictionary of Widget declarations to add to the core api. def install(): from charts.widgets.chart_view import BarChart, LineChart return { 'BarChart': BarChart, 'LineCart': LineChart, } """ from enamlnative.widgets import api for plugin in self.get_plugins(group='enaml_native_widgets'): get_widgets = plugin.load() for name, widget in iter(get_widgets()): #: Update the core api with these widgets setattr(api, name, widget) def load_plugin_factories(self): """ Pull widgets added via plugins using the `enaml_native_ios_factories` or `enaml_native_android_factories` entry points. The entry point function must return a dictionary of Widget declarations to add to the factories for this platform. def install(): return { 'MyWidget':my_widget_factory, # etc... } """ raise NotImplementedError
class OccLoadShape(OccShape, ProxyLoadShape): #: Update the class reference reference = set_default('https://dev.opencascade.org/doc/refman/html/' 'class_topo_d_s___shape.html') #: The shape created shape = Instance(BRepBuilderAPI_Transform) def create_shape(self): """ Create the shape by loading it from the given path. """ shape = self.load_shape() t = self.get_transform() self.shape = BRepBuilderAPI_Transform(shape, t, False) def get_transform(self): d = self.declaration t = gp_Trsf() #p = d.position t.SetTransformation(gp_Ax3(d.axis)) #gp_Vec(p.X(), p.Y(), p.Z())) return t def load_shape(self): d = self.declaration if not os.path.exists(d.path): raise ValueError("Can't load shape from `{}`, " "the path does not exist".format(d.path)) path, ext = os.path.splitext(d.path) name = ext[1:] if d.loader == 'auto' else d.loader loader = getattr(self, 'load_{}'.format(name.lower())) return loader(d.path) def load_brep(self, path): """ Load a brep model """ shape = TopoDS_Shape() builder = BRep_Builder() breptools_Read(shape, path, builder) return shape def load_iges(self, path): """ Load an iges model """ reader = IGESControl_Reader() status = reader.ReadFile(path) if status != IFSelect_RetDone: raise ValueError("Failed to load: {}".format(path)) reader.PrintCheckLoad(False, IFSelect_ItemsByEntity) reader.PrintCheckTransfer(False, IFSelect_ItemsByEntity) ok = reader.TransferRoots() return reader.Shape(1) def load_step(self, path): """ Alias for stp """ return self.load_stp(path) def load_stp(self, path): """ Load a stp model """ reader = STEPControl_Reader() status = reader.ReadFile(path) if status != IFSelect_RetDone: raise ValueError("Failed to load: {}".format(path)) reader.PrintCheckLoad(False, IFSelect_ItemsByEntity) reader.PrintCheckTransfer(False, IFSelect_ItemsByEntity) ok = reader.TransferRoot() return reader.Shape(1) def load_stl(self, path): """ Load a stl model """ reader = StlAPI_Reader() shape = TopoDS_Shape() reader.Read(shape, path) return shape # ------------------------------------------------------------------------- # ProxyLoadShape API # ------------------------------------------------------------------------- def set_path(self, path): self.create_shape() def set_loader(self, loader): self.create_shape()
class DeviceProtocol(Model): #: The declaration that defined this protocol declaration = Typed(extensions.DeviceProtocol).tag(config=True) #: The active protocol transport = Instance(DeviceTransport) #: The protocol specific config config = Instance(Model, ()).tag(config=True) def connection_made(self): """ This is called when a connection is made to the device. Use this to send any initialization commands before the job starts. """ raise NotImplementedError def set_pen(self, p): """ Set the pen or tool that should be used. Parameters ---------- p: int The pen or tool number to use. """ def set_force(self, f): """ Set the force the device should use. Parameters ---------- f: int The force setting value to send to the device """ def set_velocity(self, v): """ Set the force the device should use. Parameters ---------- v: int The force setting value to send to the device """ def move(self, x, y, z, absolute=True): """ Called when the device position is updated. """ raise NotImplementedError def write(self, data): """ Call this to write data using the underlying transport. This should typically not be overridden. """ if self.transport is not None: self.transport.write(data) def data_received(self, data): """ Called when the device replies back with data. This can occur at any time as communication is asynchronous. The protocol should handle as needed. Parameters ---------- data """ log.debug("data received: {}".format(data)) def finish(self): """ Called when processing all of the paths of the job are complete. Use this to send any finalization commands. """ pass def connection_lost(self): """ Called the connection to the device is dropped or failed to connect. No more data can be written when this is called. """ pass
class WxNotebook(WxConstraintsWidget, ProxyNotebook): """ A Wx implementation of an Enaml ProxyNotebook. """ #: A reference to the widget created by the proxy. widget = Instance((wxPreferencesNotebook, wxDocumentNotebook)) #-------------------------------------------------------------------------- # Initialization API #-------------------------------------------------------------------------- def create_widget(self): """ Create the underlying wx notebook widget. """ if self.declaration.tab_style == 'preferences': w = wxPreferencesNotebook(self.parent_widget()) else: style = aui.AUI_NB_SCROLL_BUTTONS w = wxDocumentNotebook(self.parent_widget(), agwStyle=style) self.widget = w def init_widget(self): """ Create and initialize the notebook control """ super(WxNotebook, self).init_widget() d = self.declaration self.set_tab_style(d.tab_style) self.set_tab_position(d.tab_position) self.set_tabs_closable(d.tabs_closable) self.set_tabs_movable(d.tabs_movable) def init_layout(self): """ Handle the layout initialization for the notebook. """ super(WxNotebook, self).init_layout() widget = self.widget for page in self.pages(): widget.AddWxPage(page) widget.Bind(EVT_COMMAND_LAYOUT_REQUESTED, self.on_layout_requested) #-------------------------------------------------------------------------- # Utility Methods #-------------------------------------------------------------------------- def pages(self): """ Get the pages defined for the notebook. """ for p in self.declaration.pages(): yield p.proxy.widget or None #-------------------------------------------------------------------------- # Child Events #-------------------------------------------------------------------------- def child_added(self, child): """ Handle the child added event for a WxNotebook. """ super(WxNotebook, self).child_added(child) if isinstance(child, WxPage): for index, dchild in enumerate(self.children()): if child is dchild: self.widget.InsertWxPage(index, child.widget) def child_removed(self, child): """ Handle the child removed event for a WxNotebook. """ super(WxNotebook, self).child_removed(child) if isinstance(child, WxPage): self.widget().RemoveWxPage(child.widget) self.size_hint_updated() #-------------------------------------------------------------------------- # Event Handlers #-------------------------------------------------------------------------- def on_layout_requested(self, event): """ Handle the layout request event from a child page. """ self.size_hint_updated() #-------------------------------------------------------------------------- # ProxyNotebook API #-------------------------------------------------------------------------- def set_tab_style(self, style): """ Set the tab style for the underlying widget. """ # Changing the tab style on wx is not supported pass def set_tab_position(self, position): """ Set the position of the tab bar in the widget. """ # Tab position changes only supported on the document notebook. widget = self.widget if isinstance(widget, wxDocumentNotebook): flags = widget.GetAGWWindowStyleFlag() flags &= ~_TAB_POSITION_MASK flags |= _TAB_POSITION_MAP[position] widget.SetAGWWindowStyleFlag(flags) widget.Refresh() # Avoids rendering artifacts def set_tabs_closable(self, closable): """ Set whether or not the tabs are closable. """ # Closable tabs are only supported on the document notebook. widget = self.widget if isinstance(widget, wxDocumentNotebook): flags = widget.GetAGWWindowStyleFlag() if closable: flags |= aui.AUI_NB_CLOSE_ON_ALL_TABS else: flags &= ~aui.AUI_NB_CLOSE_ON_ALL_TABS widget.SetAGWWindowStyleFlag(flags) def set_tabs_movable(self, movable): """ Set whether or not the tabs are movable. """ # Movable tabs are only supported on the document notebook. widget = self.widget if isinstance(widget, wxDocumentNotebook): flags = widget.GetAGWWindowStyleFlag() if movable: flags |= aui.AUI_NB_TAB_MOVE else: flags &= ~aui.AUI_NB_TAB_MOVE widget.SetAGWWindowStyleFlag(flags)
class DevicePlugin(Plugin): """ Plugin for configuring, using, and communicating with a device. """ #: Protocols registered in the system protocols = List(extensions.DeviceProtocol) #: Transports transports = List(extensions.DeviceTransport) #: Drivers registered in the system drivers = List(extensions.DeviceDriver) #: Filters registered in the system filters = List(extensions.DeviceFilter) #: Devices configured devices = List(Device).tag(config=True) #: Current device device = Instance(Device).tag(config=True) # ------------------------------------------------------------------------- # Plugin API # ------------------------------------------------------------------------- def start(self): """ Load all the plugins the device is dependent on """ w = self.workbench plugins = [] with enaml.imports(): from .transports.raw.manifest import RawFdManifest from .transports.serialport.manifest import SerialManifest from .transports.printer.manifest import PrinterManifest from .transports.disk.manifest import FileManifest from inkcut.device.protocols.manifest import ProtocolManifest from inkcut.device.drivers.manifest import DriversManifest from inkcut.device.filters.manifest import FiltersManifest from inkcut.device.pi.manifest import PiManifest plugins.append(RawFdManifest) plugins.append(SerialManifest) plugins.append(PrinterManifest) plugins.append(FileManifest) plugins.append(ProtocolManifest) plugins.append(DriversManifest) plugins.append(FiltersManifest) plugins.append(PiManifest) for Manifest in plugins: w.register(Manifest()) #: This refreshes everything else self._refresh_extensions() #: Restore state after plugins are loaded super(DevicePlugin, self).start() def submit(self, job): """ Send the given job to the device and restart all stats """ job.info.reset() job.info.started = datetime.now() return self.device.submit(job) def _default_device(self): """ If no device is loaded from the previous state, get the device from the first driver loaded. """ self._refresh_extensions() #: If a device is configured, use that if self.devices: return self.devices[0] #: Otherwise create one using the first registered driver if not self.drivers: raise RuntimeError("No device drivers were registered. " "This indicates a missing plugin.") return self.get_device_from_driver(self.drivers[0]) def _observe_device(self, change): """ Whenever the device changes, redraw """ #: Redraw plugin = self.workbench.get_plugin('inkcut.job') plugin.refresh_preview() # ------------------------------------------------------------------------- # Device Driver API # ------------------------------------------------------------------------- def get_device_from_driver(self, driver): """ Load the device driver. This generates the device using the factory function the DeviceDriver specifies and assigns the protocols and transports based on the filters given by the driver. Parameters ---------- driver: inkcut.device.extensions.DeviceDriver The DeviceDriver declaration to use to create a device. Returns ------- device: inkcut.device.plugin.Device The actual device object that will be used for communication and processing the jobs. """ # Set the protocols based on the declaration transports = [ t for t in self.transports if not driver.connections or t.id == 'disk' or t.id in driver.connections ] # Set the protocols based on the declaration protocols = [ p for p in self.protocols if not driver.protocols or p.id in driver.protocols ] # Generate the device return driver.factory(driver, transports, protocols) # ------------------------------------------------------------------------- # Device Extensions API # ------------------------------------------------------------------------- def _refresh_extensions(self): """ Refresh all extensions provided by the DevicePlugin """ self._refresh_protocols() self._refresh_transports() self._refresh_drivers() self._refresh_filters() def _refresh_protocols(self): """ Reload all DeviceProtocols registered by any Plugins Any plugin can add to this list by providing a DeviceProtocol extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DEVICE_PROTOCOL_POINT) protocols = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for p in extension.get_children(extensions.DeviceProtocol): protocols.append(p) #: Update self.protocols = protocols def _refresh_transports(self): """ Reload all DeviceTransports registered by any Plugins Any plugin can add to this list by providing a DeviceTransport extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point( extensions.DEVICE_TRANSPORT_POINT) transports = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for t in extension.get_children(extensions.DeviceTransport): transports.append(t) #: Update self.transports = transports def _refresh_drivers(self): """ Reload all DeviceDrivers registered by any Plugins Any plugin can add to this list by providing a DeviceDriver extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DEVICE_DRIVER_POINT) drivers = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for driver in extension.get_children(extensions.DeviceDriver): if not driver.id: driver.id = "{} {}".format(driver.manufacturer, driver.model) drivers.append(driver) drivers.sort(key=lambda d: d.id) # Update self.drivers = drivers def _refresh_filters(self): """ Reload all DeviceFilters registered by any Plugins Any plugin can add to this list by providing a DeviceFilter extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DEVICE_FILTER_POINT) filters = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for t in extension.get_children(extensions.DeviceFilter): filters.append(t) self.filters = filters # ------------------------------------------------------------------------- # Live progress API # ------------------------------------------------------------------------- def reset_preview(self): """ Clear the preview """ self._reset_preview(None) @observe('device', 'device.job') def _reset_preview(self, change): """ Redraw the preview on the screen """ view_items = [] #: Transform used by the view preview_plugin = self.workbench.get_plugin('inkcut.preview') plot = preview_plugin.live_preview t = preview_plugin.transform #: Draw the device device = self.device job = device.job if device and device.area: area = device.area view_items.append( dict(path=device.transform(device.area.path * t), pen=plot.pen_device, skip_autorange=True)) if job and job.material: # Also observe any change to job.media and job.device view_items.extend([ dict(path=device.transform(job.material.path * t), pen=plot.pen_media, skip_autorange=True), dict(path=device.transform(job.material.padding_path * t), pen=plot.pen_media_padding, skip_autorange=True) ]) #: Update the plot preview_plugin.set_live_preview(*view_items) @observe('device.position') def _update_preview(self, change): """ Watch the position of the device as it changes. """ if change['type'] == 'update' and self.device.job: x, y, z = change['value'] preview_plugin = self.workbench.get_plugin('inkcut.preview') preview_plugin.live_preview.update(change['value'])
class OccTransform(OccOperation, ProxyTransform): reference = set_default('https://dev.opencascade.org/doc/refman/html/' 'classgp___trsf.html') _old_shape = Instance(OccShape) def get_transform(self): d = self.declaration result = gp_Trsf() #: TODO: Order matters... how to configure it??? if d.operations: for op in d.operations: t = gp_Trsf() if isinstance(op, Translate): t.SetTranslation(gp_Vec(op.x, op.y, op.z)) elif isinstance(op, Rotate): t.SetRotation( gp_Ax1(gp_Pnt(*op.point), gp_Dir(*op.direction)), op.angle) elif isinstance(op, Mirror): Ax = gp_Ax2 if op.plane else gp_Ax1 t.SetMirror(Ax(gp_Pnt(*op.point), gp_Dir(op.x, op.y, op.z))) elif isinstance(op, Scale): t.SetScale(gp_Pnt(*op.point), op.s) result.Multiply(t) else: axis = gp_Ax3() axis.SetDirection(d.direction.proxy) result.SetTransformation(axis) result.SetTranslationPart(gp_Vec(*d.position)) if d.rotation: t = gp_Trsf() t.SetRotation(gp_Ax1(d.position.proxy, d.direction.proxy), d.rotation) result.Multiply(t) return result def update_shape(self, change=None): d = self.declaration #: Get the shape to apply the tranform to if d.shape: make_copy = True original = coerce_shape(d.shape) else: # Use the first child make_copy = False child = self.get_first_child() if child is None: raise ValueError("Transform has no shape to transform %s" % d) original = child.shape t = self.get_transform() transform = BRepBuilderAPI_Transform(original, t, make_copy) shape = transform.Shape() # Convert it back to the original type self.shape = Topology.cast_shape(shape) def set_shape(self, shape): if self._old_shape: self._old_shape.unobserve('shape', self.update_shape) self._old_shape = shape.proxy self._old_shape.observe('shape', self.update_shape) def set_translate(self, translation): self.update_shape() def set_rotate(self, rotation): self.update_shape() def set_scale(self, scale): self.update_shape() def set_mirror(self, axis): self.update_shape()