def close_file(self, event): """ Close the file with the given path and remove it from the document list. If multiple documents with the same file are open this only closes the first one it finds. """ if isinstance(event, ExecutionEvent): path = event.parameters.get('path') else: path = event # Default to current document if path is None: path = self.active_document.name docs = self.documents opened = [d for d in docs if d.name == path] if not opened: return log.debug("Closing '{}'".format(path)) doc = opened[0] self.documents.remove(doc) #: If we removed al of them if not self.documents: self.documents = self._default_documents() #: If we closed the active document if self.active_document == doc: self.active_document = self.documents[0]
def lineReceived(self, line): try: line = line.decode() log.debug(f"render | out | {line}") response = jsonpickle.loads(line) except Exception as e: response = {} #: Special case for startup response_id = response.get('id') if response_id == 'window_id': self.window_id = response['result'] self.restarts = 0 # Clear the restart count elif response_id == 'render_error': self.errors = response['error']['message'] elif response_id == 'render_ok': self.errors = "" elif response_id == 'capture_output': # Script output capture it self.output = response['result'].split("\n") elif response_id is not None: # Lookup the deferred object that should be stored for this id # when it is called and invoke the callback or errback based on the # result d = self._responses.get(response_id) if d is not None: del self._responses[response_id] error = response.get('error') if error is not None: d.errback(error) else: d.callback(response.get('result')) else: # Append to output self.output.append(line)
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 = [] typemap = {} for extension in sorted(point.extensions, key=lambda ext: ext.rank): for d in extension.get_children(extensions.SettingsPage): #: Save it settings_pages.append(d) #: Update the type map plugin = self.workbench.get_plugin(d.plugin_id) t = type(getattr(plugin, d.model) if d.model else plugin) typemap[t] = d.factory() #: Update items log.debug("Updating settings pages: {}".format(settings_pages)) self.settings_typemap = typemap self.settings_pages = settings_pages
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 = {'main': [], 'left': [], 'right': [], 'bottom': [], 'top': []} 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 errReceived(self, data): if b'XCB error' in data: return for line in data.split(b"\n"): try: log.debug(f"render | err | {line.encode()}") except: pass
def _default_source(self): """ Load the document from the path given by `name`. If it fails to load, nothing will be returned and an error will be set. """ try: log.debug("Loading '{}' from disk.".format(self.name)) with open(self.name) as f: return f.read() except Exception as e: self.errors = [str(e)] return ""
def restart(self): self.window_id = 0 self.restarts += 1 # TODO: 100 is probably excessive if self.restarts > self.max_retries: plugin = self.plugin plugin.workbench.message_critical( "Viewer failed to start", "Could not get the viewer to start after several attempts.") raise RuntimeError( "renderer | Failed to successfully start renderer aborting!") log.debug(f"Attempting to restart viewer {self.process}") deferred_call(self.start)
def err_received(self, data): """ Catch and log error output attempting to decode it """ for line in data.split(b"\n"): if not line: continue if line.startswith(b"QWidget::") or line.startswith(b"QPainter::"): continue try: line = line.decode() log.debug(f"render | err | {line}") if self.document: self.document.errors.append(line) except Exception as e: log.debug(f"render | err | {line}")
def send_message(self, method, *args, **kwargs): # Defer until it's ready if not self.transport: log.debug('renderer | message not ready deferring') timed_call(0, self.send_message, method, *args, **kwargs) return _id = kwargs.pop('_id') request = { 'jsonrpc': '2.0', 'method': method, 'params': args or kwargs } if _id is not None: request['id'] = _id log.debug(f'renderer | sent | {request}') self.transport.write(jsonpickle.dumps(request).encode() + b'\r\n')
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
def export(self, event): """ Export the current model to stl """ options = event.parameters.get('options') if not options: raise ValueError("An export `options` parameter is required") # Pickle the configured exporter and send it over cmd = [sys.executable] if not sys.executable.endswith('declaracad'): cmd.extend(['-m', 'declaracad']) data = jsonpickle.dumps(options) assert data != 'null', f"Exporter failed to serialize: {options}" cmd.extend(['export', data]) log.debug(" ".join(cmd)) protocol = ProcessLineReceiver() loop = asyncio.get_event_loop() deferred_call(loop.subprocess_exec, lambda: protocol, *cmd) return protocol
def send_message(self, method, *args, **kwargs): # Defer until it's ready if not self.transport or not self.window_id: #log.debug('renderer | message not ready deferring') timed_call(1000, self.send_message, method, *args, **kwargs) return _id = kwargs.pop('_id') _silent = kwargs.pop('_silent', False) request = { 'jsonrpc': '2.0', 'method': method, 'params': args or kwargs } if _id is not None: request['id'] = _id if not _silent: log.debug(f'renderer | sent | {request}') encoded_msg = jsonpickle.dumps(request).encode() + b'\r\n' deferred_call(self.transport.write, encoded_msg)
def open_file(self, event): """ Open a file from the local filesystem """ path = event.parameters['path'] #: Check if the document is already open for doc in self.documents: if doc.name == path: self.active_document = doc return log.debug("Opening '{}'".format(path)) #: Otherwise open it doc = Document(name=path, unsaved=False) with open(path) as f: doc.source = f.read() self.documents.append(doc) self.active_document = doc editor = self.get_editor() if editor: editor.set_text(doc.source)
def export(self, event): """ Export the current model to stl """ from twisted.internet import reactor options = event.parameters.get('options') if not options: raise ValueError("An export `options` parameter is required") # Pickle the configured exporter and send it over cmd = [sys.executable] if not sys.executable.endswith('declaracad'): cmd.extend(['-m', 'declaracad']) data = jsonpickle.dumps(options) assert data != 'null', f"Exporter failed to serialize: {options}" cmd.extend(['export', data]) log.debug(" ".join(cmd)) protocol = ProcessLineReceiver() reactor.spawnProcess(protocol, sys.executable, args=cmd, env=os.environ) return protocol
def close_file(self, event): """ Close the file with the given path and remove it from the document list. If multiple documents with the same file are open this only closes the first one it finds. """ if isinstance(event, ExecutionEvent): path = event.parameters.get('path') else: path = event # Default to current document if path is None: path = self.active_document.name docs = self.documents opened = [d for d in docs if d.name == path] if not opened: return log.debug("Closing '%s'", path) doc = opened[0] self.documents.remove(doc) # If any viewer was bound to this document, unbind it viewer_plugin = self.workbench.get_plugin('declaracad.viewer') for viewer in viewer_plugin.get_viewers(): if viewer.document == doc: viewer.document = None # If we removed all of them create a new empty one if not self.documents: self.documents = self._default_documents() self.active_document = self.documents[0] # If we closed the active document elif self.active_document == doc: self.active_document = self.documents[0]
def _observe_settings_page(self, change): log.debug("Settings page: {}".format(change))
def data_received(self, data): line = data.decode() try: response = jsonpickle.loads(line) # log.debug(f"viewer | resp | {response}") except Exception as e: log.debug(f"viewer | out | {line.rstrip()}") response = {} doc = self.document if not isinstance(response, dict): log.debug(f"viewer | out | {response.rstrip()}") return #: Special case for startup response_id = response.get('id') if response_id == 'window_id': self.window_id = response['result'] self.restarts = 0 # Clear the restart count return elif response_id == 'keep_alive': return elif response_id == 'invoke_command': command_id = response.get('command_id') parameters = response.get('parameters', {}) log.debug(f"viewer | out | {command_id}({parameters})") self.plugin.workbench.invoke_command(command_id, parameters) elif response_id == 'render_error': if doc: doc.errors.extend(response['error']['message'].split("\n")) return elif response_id == 'render_success': if doc: doc.errors = [] return elif response_id == 'capture_output': # Script output capture it if doc: doc.output = response['result'].split("\n") return elif response_id == 'shape_selection': #: TODO: Do something with this? if doc: doc.output.append(str(response['result'])) return elif response_id is not None: # Lookup the deferred object that should be stored for this id # when it is called and invoke the callback or errback based on the # result d = self._responses.get(response_id) if d is not None: del self._responses[response_id] try: error = response.get('error') if error is not None: if doc: doc.errors.extend( error.get('message', '').split("\n")) d.add_done_callback(error) else: d.add_done_callback(response.get('result')) return except Exception as e: log.warning("RPC response not properly handled!") log.exception(e) else: log.warning("Got unexpected reply") # else we got a response from something else, possibly an error? if 'error' in response and doc: doc.errors.extend(response['error'].get('message', '').split("\n")) doc.output.append(line) elif 'message' in response and doc: doc.output.extend(response['message'].split("\n")) elif doc: # Append to output doc.output.extend(line.split("\n"))
def _update_area_layout(self, change): """ When a document is opened or closed, add or remove it from the currently active TabLayout. The layout update is deferred so it fires after the items are updated by the Looper. """ if change['type'] == 'create': return #: Get the dock area area = self.get_dock_area() #: Refresh the dock items #area.looper.iterable = self.documents[:] #: Determine what change to apply removed = set() added = set() if change['type'] == 'container': op = change['operation'] if op in ['append', 'insert']: added = set([change['item']]) elif op == 'extend': added = set(change['items']) elif op in ['pop', 'remove']: removed = set([change['item']]) elif change['type'] == 'update': old = set(change['oldvalue']) new = set(change['value']) #: Determine which changed removed = old.difference(new) added = new.difference(old) elif change['type'] == 'manual': old = set([self.active_document]) new = set(self.documents) #: Determine which changed removed = old.difference(new) added = new.difference(old) #: Update operations to apply ops = [] #: Remove any old items for doc in removed: ops.append(RemoveItem(item='editor-item-{}'.format(doc.name))) #: Add any new items for doc in added: log.debug("Adding: editor-item-{}".format(doc.name)) targets = [ 'editor-item-{}'.format(d.name) for d in self.documents if d.name != doc.name ] item = EditorDockItem(area, plugin=self, doc=doc) ops.append( InsertTab(item=item.name, target=targets[0] if targets else '')) #: Now apply all layout update operations area.update_layout(ops) self.save_dock_area(change)
def _update_area_layout(self, change): """ When a document is opened or closed, add or remove it from the currently active TabLayout. The layout update is deferred so it fires after the items are updated by the Looper. """ if change['type'] == 'create': return #: Get the dock area area = self.get_dock_area() #: Refresh the dock items #area.looper.iterable = self.documents[:] #: Determine what change to apply removed = set() added = set() if change['type'] == 'container': op = change['operation'] if op in ['append', 'insert']: added = set([change['item']]) elif op == 'extend': added = set(change['items']) elif op in ['pop', 'remove']: removed = set([change['item']]) elif change['type'] == 'update': old = set(change['oldvalue']) new = set(change['value']) #: Determine which changed removed = old.difference(new) added = new.difference(old) elif change['type'] == 'load': removed = {item.doc for item in self.get_editor_items()} added = set(self.documents) #: Update operations to apply ops = [] removed_targets = set() #: Remove any old items for doc in removed: for item in self.get_editor_items(): if item.doc == doc: removed_targets.add(item.name) ops.append(RemoveItem(item=item.name)) # Remove ops if ops: log.debug(ops) area.update_layout(ops) # Add each one at a time targets = set([ item.name for item in area.dock_items() if (item.name.startswith("editor-item") and item.name not in removed_targets) ]) log.debug("Editor added=%s removed=%s targets=%s", added, removed, targets) # Sort documents so active is last so it's on top when we restore # from a previous state for doc in sorted(added, key=lambda d: int(d == self.active_document)): item = create_editor_item(area, plugin=self, doc=doc) if targets: op = InsertTab(item=item.name, target=list(targets)[-1]) try: area.update_layout(op) except Exception as e: # If it fails to add as a tab just insert it log.exception(e) op = InsertItem(item=item.name) area.update_layout(op) else: op = InsertItem(item=item.name) area.update_layout(op) targets.add(item.name) # Now save it self.save_dock_area(change)