├── .gitignore ├── README.md ├── counter.py └── pyract ├── __init__.py ├── model.py └── view.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyract 2 | 3 | A view library (`pyract.view`) inspired by React, but for Gtk+. A model library (`pyract.model`) inspired by MobX, but for Python. 4 | 5 | See `counter.py` for a heavily-commented demo. 6 | -------------------------------------------------------------------------------- /counter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Sam Parkinson 2 | # 3 | # This file is part of Pyract. 4 | # 5 | # Pyract is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Pyract is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Pyract. If not, see . 17 | 18 | from gi.repository import Gtk 19 | from pyract.view import run, Component, Node, load_css 20 | from pyract.model import ObservableModel, ObservableValue, ModelField 21 | 22 | 23 | # Let's create a model to back our application. Since it is Observable, it 24 | # will tell pyract to re-render our UI when it changes. 25 | class AppModel(ObservableModel): 26 | # The ModelField tells the model to create us an ObservableValue and set 27 | # to 0. ObservableValues lets us re-render the UI when the value changes. 28 | counter = ModelField(ObservableValue, 0) 29 | 30 | def increment(self): 31 | self.counter.value = self.counter.value + 1 32 | 33 | 34 | # Components are similar to in React. 35 | class AppComponent(Component): 36 | # Our render method can return a Node or list of Nodes. 37 | def render(self, model: AppModel): 38 | # Nodes are a type followed by kwargs. When a component is 39 | # re-rendered, then the Node trees get diffed and only the changed 40 | # Nodes and properties are updated. 41 | # The type is either a Gtk.Widget or pyract.Component subclass. 42 | return Node(Gtk.ApplicationWindow, 43 | title='My Counter App', 44 | children=[ 45 | Node(Gtk.Box, orientation=Gtk.Orientation.VERTICAL, 46 | children=[ 47 | # The class_names prop adds the appropriate classes 48 | Node(Gtk.Label, class_names=['counter-label'], 49 | label=str(model.counter.value)), 50 | Node(Gtk.Button, label='Increment Counter', 51 | class_names=['suggested-action', 'bottom-button'], 52 | # Hide the button when the counter gets to ten 53 | visible=model.counter.value < 10, 54 | # "signal__" props are the same as connecting a 55 | # GObject signal normally 56 | signal__clicked=self._button_clicked_cb), 57 | # Add a reset button, but only show it when counter == 10 58 | Node(Gtk.Button, label='Reset', 59 | class_names=['destructive-action', 'bottom-button'], 60 | visible=model.counter.value >= 10, 61 | signal__clicked=self._reset_clicked_cb) 62 | ]) 63 | ]) 64 | 65 | # Signal handlers are just like in normal Gtk+ 66 | def _button_clicked_cb(self, button): 67 | # Access the props using self.props 68 | self.props['model'].increment() 69 | 70 | def _reset_clicked_cb(self, button): 71 | self.props['model'].counter.value = 0 72 | 73 | 74 | # Adding CSS is really easy: 75 | load_css(''' 76 | .counter-label { 77 | font-size: 100px; 78 | padding: 20px; 79 | } 80 | .bottom-button { 81 | margin: 10px; 82 | } 83 | ''') 84 | 85 | 86 | # The run function takes a node, and runs the app 87 | run(Node(AppComponent, model=AppModel()), 'today.sam.pyract.counter') 88 | -------------------------------------------------------------------------------- /pyract/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Sam Parkinson 2 | # 3 | # This file is part of Pyract. 4 | # 5 | # Pyract is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Pyract is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Pyract. If not, see . 17 | 18 | # Ha, you can't make this blank file proprietary now! 19 | -------------------------------------------------------------------------------- /pyract/model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Sam Parkinson 2 | # 3 | # This file is part of Pyract. 4 | # 5 | # Pyract is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Pyract is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Pyract. If not, see . 17 | 18 | import json 19 | from gi.repository import GObject 20 | from typing import Generic, Union, Dict, List 21 | 22 | 23 | PopoType = Union[str, int, float, bool, dict, list] 24 | 25 | 26 | class Observable(GObject.GObject): 27 | changed_signal = GObject.Signal('changed') 28 | 29 | def serialize(self) -> PopoType: 30 | raise NotImplimentedError() 31 | 32 | def deserialize(self, value: PopoType): 33 | raise NotImplimentedError() 34 | 35 | 36 | class ObservableValue(Observable): 37 | def __init__(self, value): 38 | super().__init__() 39 | self._value = value 40 | 41 | @property 42 | def value(self): 43 | return self._value 44 | 45 | @value.setter 46 | def value(self, new_value): 47 | if self._value == new_value: 48 | return 49 | self._value = new_value 50 | self.changed_signal.emit() 51 | 52 | def serialize(self) -> PopoType: 53 | return self.value 54 | 55 | def deserialize(self, value: PopoType): 56 | self.value = value 57 | 58 | 59 | class ObservableModel(Observable): 60 | def __init__(self, **kwargs): 61 | super().__init__() 62 | 63 | for k, v in vars(type(self)).items(): 64 | if isinstance(v, ModelField): 65 | setattr(self, k, v.create()) 66 | 67 | for k, v in kwargs.items(): 68 | getattr(self, k).value = v 69 | 70 | def _attribute_changed_cb(self, value): 71 | self.changed_signal.emit() 72 | 73 | def __setattr__(self, k, new): 74 | old = None 75 | if hasattr(self, k): 76 | old = getattr(self, k) 77 | if isinstance(old, Observable) and old != getattr(type(self), k): 78 | old.disconnect_by_func(self._attribute_changed_cb) 79 | 80 | if not isinstance(new, Observable): 81 | raise ValueError( 82 | 'Can not replace observable key {} with ' 83 | 'non-observable object {}'.format(k, new)) 84 | 85 | if isinstance(new, Observable): 86 | new.changed_signal.connect(self._attribute_changed_cb) 87 | 88 | if old != new: 89 | self.changed_signal.emit() 90 | super().__setattr__(k, new) 91 | 92 | def serialize(self) -> Dict[str, PopoType]: 93 | ret = {} 94 | for k, v in vars(type(self)).items(): 95 | if isinstance(v, ModelField): 96 | ret[k] = getattr(self, k).serialize() 97 | return ret 98 | 99 | def serialize_to_path(self, path): 100 | j = self.serialize() 101 | with open(path, 'w') as f: 102 | json.dump(j, f) 103 | 104 | def deserialize(self, value: Dict[str, PopoType]): 105 | for k, v in value.items(): 106 | getattr(self, k).deserialize(v) 107 | 108 | def deserialize_from_path(self, path): 109 | with open(path) as f: 110 | j = json.load(f) 111 | self.deserialize(j) 112 | 113 | 114 | class ModelField(): 115 | def __init__(self, type_, *args, **kwargs): 116 | self._type = type_ 117 | self._args = args 118 | self._kwargs = kwargs 119 | 120 | if not issubclass(type_, Observable): 121 | raise ValueError('ModelFields type_ must be Observable subclass') 122 | 123 | def create(self): 124 | return self._type(*self._args, **self._kwargs) 125 | 126 | 127 | class ObservableList(ObservableValue): 128 | def __init__(self, type_, value=None, *args, **kwargs): 129 | super().__init__(value or [], *args, **kwargs) 130 | self._type = type_ 131 | for v in self.value: 132 | v.changed_signal.connect(self._item_changed_cb) 133 | 134 | def _item_changed_cb(self, item): 135 | self.changed_signal.emit() 136 | 137 | @property 138 | def value(self): 139 | return self._value 140 | 141 | @value.setter 142 | def value(self, new_value): 143 | assert(isinstance(new_value, list)) 144 | if self._value == new_value: 145 | return 146 | for item in new_value: 147 | if item not in self._value: 148 | item.changed_signal.connect(self._item_changed_cb) 149 | for item in self._value: 150 | if item not in new_value: 151 | item.disconnect_by_func(self._item_changed_cb) 152 | 153 | def __getitem__(self, y): return self.value[y] 154 | def __iter__(self): return iter(self.value) 155 | def __len__(self): return len(self.value) 156 | def __bool__(self): return bool(self.value) 157 | 158 | def append(self, item): 159 | self.value.append(item) 160 | item.changed_signal.connect(self._item_changed_cb) 161 | self.changed_signal.emit() 162 | 163 | def insert(self, index, item): 164 | self.value.insert(index, item) 165 | item.changed_signal.connect(self._item_changed_cb) 166 | self.changed_signal.emit() 167 | 168 | def clear(self): 169 | for item in self: 170 | item.disconnect_by_func(self._item_changed_cb) 171 | self.value.clear() 172 | self.changed_signal.emit() 173 | 174 | def pop(self, index=None): 175 | item = self.value.pop(index) 176 | item.disconnect_by_func(self._item_changed_cb) 177 | self.changed_signal.emit() 178 | return item 179 | 180 | def serialize(self) -> List[PopoType]: 181 | return [v.serialize() for v in self] 182 | 183 | def deserialize(self, value: List[PopoType]): 184 | self.clear() 185 | for v in value: 186 | ins = self._type() 187 | ins.deserialize(v) 188 | self.append(ins) 189 | -------------------------------------------------------------------------------- /pyract/view.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Sam Parkinson 2 | # 3 | # This file is part of Pyract. 4 | # 5 | # Pyract is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Pyract is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Pyract. If not, see . 17 | 18 | import sys 19 | from gi.repository import Gtk, Gdk, GObject 20 | from typing import Union, List 21 | 22 | from .model import Observable 23 | 24 | 25 | class Node(tuple): 26 | def __new__(cls, type_, **kwargs): 27 | return super(Node, cls).__new__(cls, (type_, kwargs)) 28 | 29 | def __init__(self, type_, **props): 30 | self.type = type_ 31 | self.props = props 32 | # Almost a tuple, but we have this nice mutable instance prop 33 | self.instance = None 34 | 35 | def __repr__(self): 36 | return ' {} {}>'.format( 37 | self.type.__module__, self.type.__name__, 38 | self.instance, self.props) 39 | 40 | 41 | def _node_list_single_widget(nl): 42 | widgets = [] 43 | for node in (nl or []): 44 | widgets.extend(node.instance.get_widgets()) 45 | 46 | if len(widgets) > 1: 47 | raise ChildrenFormatException( 48 | 'Node list {} should have 1 widget, ' 49 | 'got {}'.format(nl, widgets)) 50 | 51 | return widgets[0] if len(widgets) else None 52 | 53 | 54 | class BaseComponent(GObject.GObject): 55 | updated_signal = GObject.Signal('updated') 56 | 57 | def __init__(self, **props): 58 | super().__init__() 59 | 60 | def update(self, updated_list=[]): 61 | pass 62 | 63 | def destroy(self): 64 | pass 65 | 66 | def get_widgets(self): 67 | return [] 68 | 69 | 70 | class RenderException(Exception): 71 | pass 72 | 73 | class ChildrenFormatException(RenderException): 74 | pass 75 | 76 | 77 | class GtkComponent(BaseComponent): 78 | def __init__(self, type_, **props): 79 | super().__init__() 80 | self._props = {} 81 | self._type = type_ 82 | self._instance = type_() 83 | 84 | if not issubclass(self._type, Gtk.Popover): 85 | # visible=True is a default prop 86 | self.update([('visible', True)]) 87 | self.update(props.items()) 88 | 89 | def update(self, updated_list=[]): 90 | for k, v in updated_list: 91 | if k.startswith('signal__'): 92 | # TODO: Unbind old handler 93 | if v is not None: 94 | self._instance.connect(k[8:], v) 95 | elif k.startswith('data__'): 96 | setattr(self._instance, k[6:], v) 97 | elif k.startswith('child__'): 98 | continue # We don't handle the child props ourself 99 | elif k == 'auto_grab_focus': 100 | if v: 101 | self._setup_auto_grab_focus() 102 | elif k == 'class_names': 103 | self._handle_class_names(v) 104 | elif k == 'size_groups': 105 | self._handle_size_groups(v) 106 | elif k == 'children': 107 | self._handle_children(v) 108 | elif k.startswith('____'): 109 | continue 110 | else: 111 | self.set_property(k, v) 112 | self._props[k] = v 113 | self.updated_signal.emit() 114 | 115 | def set_property(self, k, v): 116 | if k == 'popover' and issubclass(self._type, Gtk.MenuButton): 117 | self._instance.set_property(k, _node_list_single_widget(v)) 118 | elif k == 'image' and issubclass(self._type, Gtk.Button): 119 | self._instance.set_property(k, _node_list_single_widget(v)) 120 | else: 121 | self._instance.set_property(k, v) 122 | 123 | def __realize_cb(self, instance): 124 | instance.grab_focus() 125 | 126 | def _setup_auto_grab_focus(self): 127 | if self._instance.get_realized(): 128 | self._instance.grab_focus() 129 | else: 130 | self._instance.connect('realize', self.__realize_cb) 131 | 132 | def _handle_class_names(self, new): 133 | old = self._props.get('class_names', []) 134 | sc = self._instance.get_style_context() 135 | 136 | for cn in old: 137 | if cn not in new: 138 | sc.remove_class(cn) 139 | for cn in new: 140 | if cn not in old: 141 | sc.add_class(cn) 142 | 143 | def _handle_size_groups(self, new): 144 | old = self._props.get('size_groups', []) 145 | for sg in old: 146 | if sg not in new: 147 | sg.remove_widget(self._instance) 148 | for sg in new: 149 | if sg not in old: 150 | sg.add_widget(self._instance) 151 | 152 | def _handle_children(self, child_items): 153 | children = [] 154 | for node in child_items: 155 | children.extend(node.instance.get_widgets()) 156 | 157 | if issubclass(self._type, Gtk.Bin): 158 | if issubclass(self._type, Gtk.Window): 159 | all_children = children 160 | children = [] 161 | headers = [] 162 | for w in all_children: 163 | if isinstance(w, Gtk.HeaderBar): 164 | headers.append(w) 165 | else: 166 | children.append(w) 167 | 168 | if len(headers) > 1: 169 | raise ChildrenFormatException( 170 | 'A window may only have 1 header widget, ' 171 | 'got {}'.format(headers)) 172 | if len(headers) == 1: 173 | self._instance.set_titlebar(headers[0]) 174 | 175 | if len(children) == 1: 176 | # We don't trust the Gtk.Bin.get_child(), as it may have been 177 | # wrapped (eg. adding something to a Gtk.ScrolledWindow 178 | # can replace it with an inner Gtk.Viewport) 179 | if not hasattr(self, 'box_inner_child') \ 180 | or self.box_inner_child != children[0]: 181 | old = self._instance.get_child() 182 | if old: 183 | self._instance.remove(old) 184 | self._instance.add(children[0]) 185 | self.box_inner_child = children[0] 186 | else: 187 | raise ChildrenFormatException( 188 | 'GtkBin subclass {} should only have 1 child, got {}'.format( 189 | self._type, children)) 190 | elif issubclass(self._type, Gtk.Box): 191 | old = self._instance.get_children() 192 | for old_child in old: 193 | if old_child not in children: 194 | self._instance.remove(old_child) 195 | for i, child in enumerate(children): 196 | if child not in old: 197 | self._instance.add(child) 198 | self._instance.reorder_child(child, i) 199 | elif issubclass(self._type, (Gtk.FlowBox, Gtk.ListBox)): 200 | # Don't try and sort things while we are changing the children 201 | self._instance.set_sort_func(None) 202 | 203 | child_type = (Gtk.FlowBoxChild 204 | if issubclass(self._type, Gtk.FlowBox) 205 | else Gtk.ListBoxRow) 206 | 207 | old = self._instance.get_children() 208 | for old_child in old: 209 | if old_child not in children: 210 | self._instance.remove(old_child) 211 | for i, child in enumerate(children): 212 | if child not in old: 213 | if not isinstance(child, child_type): 214 | raise ChildrenFormatException( 215 | '{Flow,List}Box children must be ' 216 | 'Gtk.{Flow,List}BoxChild respectively, ' 217 | 'got {}'.format(child)) 218 | self._instance.add(child) 219 | child.__flowbox_index = i 220 | 221 | def _flowbox_sort(a, b): 222 | return a.__flowbox_index - b.__flowbox_index 223 | 224 | self._instance.set_sort_func(_flowbox_sort) 225 | self._instance.invalidate_sort() 226 | elif issubclass(self._type, Gtk.HeaderBar): 227 | start = [] 228 | end = [] 229 | for node in child_items: 230 | if node.props.get('child__is_end'): 231 | end.extend(node.instance.get_widgets()) 232 | else: 233 | start.extend(node.instance.get_widgets()) 234 | 235 | # This is broken if children move from start->end 236 | old = self._instance.get_children() 237 | for old_child in old: 238 | if old_child not in children: 239 | self._instance.remove(old_child) 240 | for child in start: 241 | if child not in old: 242 | self._instance.pack_start(child) 243 | for child in end: 244 | if child not in old: 245 | self._instance.pack_end(child) 246 | else: 247 | if len(children): 248 | raise ChildrenFormatException( 249 | 'Widget {} should have 0 children, got {}'.format( 250 | self._type, children)) 251 | 252 | def get_widgets(self): 253 | return [self._instance] 254 | 255 | def destroy(self): 256 | self._instance.destroy() 257 | # FIXME: Destroy props['popover'], props['image'] when needed 258 | for child in (self._props.get('children') or []): 259 | child.instance.destroy() 260 | 261 | 262 | 263 | 264 | class Component(BaseComponent): 265 | def __init__(self, **props): 266 | super().__init__() 267 | self.props = {} 268 | self.state = None 269 | self._rendered_yet = False 270 | 271 | self._subtreelist = None 272 | self.update(props.items()) 273 | 274 | def _observable_changed_cb(self, observable): 275 | self.update() 276 | 277 | def update(self, updated_list=[]): 278 | for k, v in updated_list: 279 | if k.startswith('child__'): 280 | continue # We don't handle the child props ourself 281 | 282 | old = self.props.get(k) 283 | if isinstance(old, Observable): 284 | old.disconnect_by_func(self._observable_changed_cb) 285 | 286 | self.props[k] = v 287 | if isinstance(v, Observable): 288 | v.changed_signal.connect(self._observable_changed_cb) 289 | 290 | if not self._rendered_yet: 291 | if hasattr(type(self), 'State'): 292 | state_cls = getattr(type(self), 'State') 293 | self.state = state_cls() 294 | self.before_first_render(**self.props) 295 | if self.state is not None: 296 | self.state.changed_signal.connect(self._observable_changed_cb) 297 | self._rendered_yet = True 298 | 299 | new = self.render(**self.props) 300 | self._subtreelist = render_treelist(self._subtreelist, new) 301 | self.updated_signal.emit() 302 | 303 | def before_first_render(self, **props) -> None: 304 | pass 305 | 306 | def render(self, **props) -> Union[Node, List[Node]]: 307 | return [] 308 | 309 | def _get_subtreelist(self): 310 | stl = (self._subtreelist or []) 311 | if isinstance(stl, Node): 312 | stl = [stl] 313 | return stl 314 | 315 | def get_widgets(self): 316 | widgets = [] 317 | for node in self._get_subtreelist(): 318 | widgets.extend(node.instance.get_widgets()) 319 | return widgets 320 | 321 | def destroy(self): 322 | for node in self._get_subtreelist(): 323 | node.instance.destroy() 324 | 325 | 326 | def treeitem_to_key(i, v): 327 | type, props = v 328 | return '{}:{}.{}'.format( 329 | props.get('key') or i, type.__module__, type.__name__) 330 | 331 | 332 | def children_keys_dict(children): 333 | d = {} 334 | for i, v in enumerate(children): 335 | d[treeitem_to_key(i, v)] = v 336 | return d 337 | 338 | 339 | _EXCLUDED_KEYS = {'ref', 'key'} 340 | 341 | 342 | def _get_to_inflate_for_type(type_) -> List[str]: 343 | l = ['children'] 344 | if issubclass(type_, Gtk.Widget): 345 | if issubclass(type_, Gtk.MenuButton): 346 | l.append('popover') 347 | if issubclass(type_, Gtk.Button): 348 | l.append('image') 349 | return l 350 | 351 | 352 | def prop_values_equal(a, b): 353 | if a == b: 354 | return True 355 | return False 356 | 357 | 358 | def render_tree(old, new): 359 | # Split the tree input 360 | old_type, old_props = old or (None, {}) 361 | instance = old.instance if old else None 362 | new_type, new_props = new 363 | 364 | to_inflate = _get_to_inflate_for_type(new_type) 365 | for k in to_inflate: 366 | old = old_props.get(k, []) 367 | new = new_props.get(k, []) 368 | v = render_treelist(old, new) 369 | if v: 370 | new_props[k] = v 371 | 372 | if old_type == new_type: 373 | changes = [] 374 | for k in old_props.keys(): 375 | if k in _EXCLUDED_KEYS: 376 | continue 377 | if k not in new_props: 378 | changes.append((k, None)) 379 | for k, v in new_props.items(): 380 | if k in _EXCLUDED_KEYS: 381 | continue 382 | if not prop_values_equal(old_props.get(k), v): 383 | changes.append((k, v)) 384 | if changes: 385 | instance.update(changes) 386 | else: 387 | if instance is not None: 388 | instance.destroy() 389 | 390 | p = {k: v for k, v in new_props.items() if k not in _EXCLUDED_KEYS} 391 | if issubclass(new_type, Gtk.Widget): 392 | instance = GtkComponent(new_type, **p) 393 | else: 394 | instance = new_type(**p) 395 | 396 | if new_props.get('ref'): 397 | new_props['ref'](instance) 398 | 399 | node = Node(new_type, **new_props) 400 | node.instance = instance 401 | return node 402 | 403 | 404 | def render_treelist(old, new): 405 | old = old or [] 406 | if isinstance(old, Node): 407 | old = [old] 408 | if isinstance(new, Node): 409 | new = [new] 410 | old_keys = children_keys_dict(old) 411 | new_keys = children_keys_dict(new) 412 | ret = [] 413 | 414 | for k, v in old_keys.items(): 415 | if k not in new_keys: 416 | if v.instance is not None: 417 | v.instance.destroy() 418 | for i, v in enumerate(new): 419 | k = treeitem_to_key(i, v) 420 | ret.append(render_tree(old_keys.get(k), v)) 421 | 422 | return ret 423 | 424 | 425 | class _PyractApplication(Gtk.Application): 426 | def __init__(self, node, app_id): 427 | super().__init__(application_id=app_id) 428 | self._node = node 429 | 430 | def do_activate(self): 431 | type_, kwargs = self._node 432 | instance = type_(**kwargs) 433 | self._updated_cb(instance) 434 | instance.updated_signal.connect(self._updated_cb) 435 | 436 | def _updated_cb(self, instance): 437 | for widget in instance.get_widgets(): 438 | if isinstance(widget, Gtk.ApplicationWindow) and widget: 439 | self.add_window(widget) 440 | 441 | 442 | def run(node, app_id): 443 | app = _PyractApplication(node, app_id) 444 | app.run(sys.argv) 445 | 446 | 447 | def load_css(data): 448 | css_prov = Gtk.CssProvider() 449 | css_prov.load_from_data(data.encode('utf8')) 450 | Gtk.StyleContext.add_provider_for_screen( 451 | Gdk.Screen.get_default(), 452 | css_prov, 453 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 454 | --------------------------------------------------------------------------------