|
| 1 | +from kivy.adapters.dictadapter import DictAdapter |
| 2 | +from kivy.uix.listview import ListItemButton, ListItemLabel, \ |
| 3 | + CompositeListItem, ListView, SelectableView |
| 4 | +from kivy.uix.gridlayout import GridLayout |
| 5 | + |
| 6 | + |
| 7 | +from kivy.uix.label import Label |
| 8 | +from kivy.uix.textinput import TextInput |
| 9 | +from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty |
| 10 | +from kivy.uix.popup import Popup |
| 11 | +from kivy.uix.button import Button |
| 12 | +from kivy.uix.boxlayout import BoxLayout |
| 13 | +from kivy.clock import Clock |
| 14 | +from kivy.lang import Builder |
| 15 | +from kivy.event import EventDispatcher |
| 16 | +from kivy.logger import Logger |
| 17 | +from backgroundJob import BackgroundJob |
| 18 | + |
| 19 | +from random import randint |
| 20 | +Builder.load_file("DataModel.kv") |
| 21 | + |
| 22 | +integers_dict = {} |
| 23 | + |
| 24 | + |
| 25 | +class ErrorPopup(Popup): |
| 26 | + """ |
| 27 | + Popup class to display error messages |
| 28 | + """ |
| 29 | + def __init__(self, **kwargs): |
| 30 | + print kwargs |
| 31 | + super(ErrorPopup, self).__init__(**kwargs) |
| 32 | + content = BoxLayout(orientation="vertical") |
| 33 | + content.add_widget(Label(text=kwargs['text'], font_size=20)) |
| 34 | + mybutton = Button(text="Dismiss", size_hint=(1,.20), font_size=20) |
| 35 | + content.add_widget(mybutton) |
| 36 | + self.content = content |
| 37 | + self.title = kwargs["title"] |
| 38 | + self.auto_dismiss = False |
| 39 | + self.size_hint = .7, .5 |
| 40 | + self.font_size = 20 |
| 41 | + mybutton.bind(on_release=self.exit_popup) |
| 42 | + self.open() |
| 43 | + |
| 44 | + def exit_popup(self, *args): |
| 45 | + self.dismiss() |
| 46 | + |
| 47 | + |
| 48 | +class ListItemReprMixin(Label): |
| 49 | + """ |
| 50 | + repr class for ListItem Composite class |
| 51 | + """ |
| 52 | + def __repr__(self): |
| 53 | + text = self.text.encode('utf-8') if isinstance(self.text, unicode) \ |
| 54 | + else self.text |
| 55 | + return '<%s text=%s>' % (self.__class__.__name__, text) |
| 56 | + |
| 57 | + |
| 58 | +class NumericTextInput(SelectableView, TextInput): |
| 59 | + """ |
| 60 | + :class:`~kivy.uix.listview.NumericTextInput` mixes |
| 61 | + :class:`~kivy.uix.listview.SelectableView` with |
| 62 | + :class:`~kivy.uix.label.TextInput` to produce a label suitable for use in |
| 63 | + :class:`~kivy.uix.listview.ListView`. |
| 64 | + """ |
| 65 | + edit = BooleanProperty(False) |
| 66 | + |
| 67 | + def __init__(self, data_model, minval, maxval, **kwargs): |
| 68 | + self.minval = minval |
| 69 | + self.maxval = maxval |
| 70 | + |
| 71 | + super(NumericTextInput, self).__init__(**kwargs) |
| 72 | + try: |
| 73 | + self.val = int(self.text) |
| 74 | + except ValueError: |
| 75 | + error = "Only numeric value in range {0}-{1} to be used".format(minval, maxval) |
| 76 | + self.hint_text = error |
| 77 | + self.padding_x = self.width |
| 78 | + self.disabled = True |
| 79 | + self.data_model = data_model |
| 80 | + |
| 81 | + def on_touch_down(self, touch): |
| 82 | + if self.collide_point(*touch.pos) and not self.edit: |
| 83 | + self.edit = True |
| 84 | + self.select() |
| 85 | + return super(NumericTextInput, self).on_touch_down(touch) |
| 86 | + |
| 87 | + def select(self, *args): |
| 88 | + self.disabled = False |
| 89 | + self.bold = True |
| 90 | + if isinstance(self.parent, CompositeListItem): |
| 91 | + for child in self.parent.children: |
| 92 | + print child.children |
| 93 | + self.parent.select_from_child(self, *args) |
| 94 | + |
| 95 | + def deselect(self, *args): |
| 96 | + self.bold = False |
| 97 | + self.disabled = True |
| 98 | + if isinstance(self.parent, CompositeListItem): |
| 99 | + self.parent.deselect_from_child(self, *args) |
| 100 | + |
| 101 | + def select_from_composite(self, *args): |
| 102 | + self.bold = True |
| 103 | + |
| 104 | + def deselect_from_composite(self, *args): |
| 105 | + self.bold = False |
| 106 | + |
| 107 | + def on_text_validate(self, *args): |
| 108 | + |
| 109 | + try: |
| 110 | + int(self.text) |
| 111 | + |
| 112 | + if not(self.minval <= int(self.text) <= self.maxval): |
| 113 | + raise ValueError |
| 114 | + self.edit = False |
| 115 | + self.data_model.on_data_update(self.index, self.text) |
| 116 | + self.deselect() |
| 117 | + except ValueError: |
| 118 | + error_text = ("Only numeric value " |
| 119 | + "in range {0}-{1} to be used".format(self.minval, |
| 120 | + self.maxval)) |
| 121 | + ErrorPopup(title="Error", text=error_text) |
| 122 | + self.text = "" |
| 123 | + self.hint_text = error_text |
| 124 | + return |
| 125 | + |
| 126 | + def on_text_focus(self, instance, focus): |
| 127 | + if focus is False: |
| 128 | + self.text = instance.text |
| 129 | + self.edit = False |
| 130 | + self.deselect() |
| 131 | + |
| 132 | + |
| 133 | +class UpdateEventDispatcher(EventDispatcher): |
| 134 | + ''' |
| 135 | + Event dispatcher for updates in Data Model |
| 136 | + ''' |
| 137 | + def __init__(self, **kwargs): |
| 138 | + self.register_event_type('on_update') |
| 139 | + super(UpdateEventDispatcher, self).__init__(**kwargs) |
| 140 | + |
| 141 | + def on_update(self, _parent, blockname, data): |
| 142 | + Logger.debug("In UpdateEventDispatcher " |
| 143 | + "on_update {parent:%s," |
| 144 | + " blockname: %s, data:%s,}" % (_parent, blockname, data)) |
| 145 | + _parent.sync_data_callback(blockname, data) |
| 146 | + |
| 147 | + |
| 148 | +class DataModel(GridLayout): |
| 149 | + """ |
| 150 | + Uses :class:`CompositeListItem` for list item views comprised by two |
| 151 | + :class:`ListItemButton`s and one :class:`ListItemLabel`. Illustrates how |
| 152 | + to construct the fairly involved args_converter used with |
| 153 | + :class:`CompositeListItem`. |
| 154 | + """ |
| 155 | + minval = NumericProperty(0) |
| 156 | + maxval = NumericProperty(0) |
| 157 | + simulate = False |
| 158 | + time_interval = 1 |
| 159 | + dirty_thread = False |
| 160 | + dirty_model = False |
| 161 | + simulate_timer = None |
| 162 | + simulate = False |
| 163 | + dispatcher = None |
| 164 | + list_view = None |
| 165 | + _parent = None |
| 166 | + is_simulating = False |
| 167 | + blockname = "<BLOCK_NAME_NOT_SET>" |
| 168 | + |
| 169 | + def __init__(self, **kwargs): |
| 170 | + kwargs['cols'] = 2 |
| 171 | + kwargs['size_hint'] = (1.0, 1.0) |
| 172 | + super(DataModel, self).__init__(**kwargs) |
| 173 | + self.init() |
| 174 | + |
| 175 | + def init(self, simulate=False, time_interval=1, **kwargs): |
| 176 | + """ |
| 177 | + Initializes Datamodel |
| 178 | +
|
| 179 | + """ |
| 180 | + self.minval = kwargs.get("minval", self.minval) |
| 181 | + self.maxval = kwargs.get("maxval", self.maxval) |
| 182 | + self.blockname = kwargs.get("blockname", self.blockname) |
| 183 | + self.clear_widgets() |
| 184 | + self.simulate = simulate |
| 185 | + self.time_interval = time_interval |
| 186 | + dict_adapter = DictAdapter(data={}, |
| 187 | + args_converter=self.arg_converter, |
| 188 | + selection_mode='single', |
| 189 | + allow_empty_selection=True, |
| 190 | + cls=CompositeListItem |
| 191 | + ) |
| 192 | + |
| 193 | + # Use the adapter in our ListView: |
| 194 | + self.list_view = ListView(adapter=dict_adapter) |
| 195 | + self.add_widget(self.list_view) |
| 196 | + self.dispatcher = UpdateEventDispatcher() |
| 197 | + self._parent = kwargs.get('_parent', None) |
| 198 | + self.simulate_timer = BackgroundJob( |
| 199 | + "simulation", |
| 200 | + self.time_interval, |
| 201 | + self._simulate_block_values |
| 202 | + ) |
| 203 | + |
| 204 | + def clear_widgets(self, make_dirty=False, **kwargs): |
| 205 | + """ |
| 206 | + Overidden Clear widget function used while deselecting/deleting slave |
| 207 | + :param make_dirty: |
| 208 | + :param kwargs: |
| 209 | + :return: |
| 210 | + """ |
| 211 | + if make_dirty: |
| 212 | + self.dirty_model = True |
| 213 | + super(DataModel, self).clear_widgets(**kwargs) |
| 214 | + |
| 215 | + def reinit(self, **kwargs): |
| 216 | + """ |
| 217 | + Re-initializes Datamodel on change in model configuration from settings |
| 218 | + :param kwargs: |
| 219 | + :return: |
| 220 | + """ |
| 221 | + self.minval = kwargs.get("minval", self.minval) |
| 222 | + self.maxval = kwargs.get("maxval", self.maxval) |
| 223 | + time_interval = kwargs.get("time_interval", None) |
| 224 | + try: |
| 225 | + if time_interval and int(time_interval) != self.time_interval: |
| 226 | + self.time_interval = time_interval |
| 227 | + if self.is_simulating: |
| 228 | + self.simulate_timer.cancel() |
| 229 | + self.simulate_timer = BackgroundJob(self.time_interval, |
| 230 | + self._simulate_block_values) |
| 231 | + self.dirty_thread = False |
| 232 | + self.start_stop_simulation(self.simulate) |
| 233 | + except ValueError: |
| 234 | + Logger.debug("Error while reinitializing DataModel %s" % kwargs) |
| 235 | + |
| 236 | + def update_view(self): |
| 237 | + """ |
| 238 | + Updates view with listview again |
| 239 | + :return: |
| 240 | + """ |
| 241 | + if self.dirty_model: |
| 242 | + self.add_widget(self.list_view) |
| 243 | + self.dirty_model = False |
| 244 | + |
| 245 | + def arg_converter(self, index, data): |
| 246 | + """ |
| 247 | + arg converter to convert data to list view |
| 248 | + :param index: |
| 249 | + :param data: |
| 250 | + :return: |
| 251 | + """ |
| 252 | + _id = self.list_view.adapter.sorted_keys[index] |
| 253 | + return { |
| 254 | + 'text': str(_id), |
| 255 | + 'size_hint_y': None, |
| 256 | + 'height': 30, |
| 257 | + 'cls_dicts': [ |
| 258 | + { |
| 259 | + 'cls': ListItemButton, |
| 260 | + 'kwargs': {'text': str(_id)} |
| 261 | + }, |
| 262 | + { |
| 263 | + 'cls': NumericTextInput, |
| 264 | + 'kwargs': { |
| 265 | + 'data_model': self, |
| 266 | + 'minval': self.minval, |
| 267 | + 'maxval': self.maxval, |
| 268 | + 'text': str(data), |
| 269 | + 'multiline': False, |
| 270 | + 'is_representing_cls': True, |
| 271 | + |
| 272 | + |
| 273 | + } |
| 274 | + }, |
| 275 | + ] |
| 276 | + } |
| 277 | + |
| 278 | + def add_data(self, data, item_strings): |
| 279 | + """ |
| 280 | + Adds data to the Data model |
| 281 | + :param data: |
| 282 | + :param item_strings: |
| 283 | + :return: |
| 284 | + """ |
| 285 | + self.update_view() |
| 286 | + last_index = len(item_strings) |
| 287 | + if last_index in item_strings: |
| 288 | + last_index = int(item_strings[-1]) + 1 |
| 289 | + item_strings.append(last_index) |
| 290 | + self.list_view.adapter.data.update({last_index: data}) |
| 291 | + self.list_view._trigger_reset_populate() |
| 292 | + return self.list_view.adapter.data, item_strings |
| 293 | + |
| 294 | + def delete_data(self, item_strings): |
| 295 | + """ |
| 296 | + Delete data from data model |
| 297 | + :param item_strings: |
| 298 | + :return: |
| 299 | + """ |
| 300 | + selections = self.list_view.adapter.selection |
| 301 | + items_popped = [] |
| 302 | + for item in selections: |
| 303 | + index_popped = item_strings.pop(item_strings.index(int(item.text))) |
| 304 | + data_popped = self.list_view.adapter.data.pop(int(item.text), None) |
| 305 | + self.list_view.adapter.update_for_new_data() |
| 306 | + self.list_view._trigger_reset_populate() |
| 307 | + items_popped.append(index_popped) |
| 308 | + return items_popped, self.list_view.adapter.data |
| 309 | + |
| 310 | + def on_selection_change(self, item): |
| 311 | + pass |
| 312 | + |
| 313 | + def on_data_update(self, index, data): |
| 314 | + """ |
| 315 | + Call back function to update data when data is changed in the list view |
| 316 | + :param index: |
| 317 | + :param data: |
| 318 | + :return: |
| 319 | + """ |
| 320 | + self.list_view.adapter.data.update({index: data}) |
| 321 | + self.list_view._trigger_reset_populate() |
| 322 | + self.dispatcher.dispatch('on_update', self._parent, self.blockname, |
| 323 | + self.list_view.adapter.data) |
| 324 | + |
| 325 | + def refresh(self, data={}): |
| 326 | + """ |
| 327 | + Data model refresh function to update when the view when slave is |
| 328 | + selected |
| 329 | + :param data: |
| 330 | + :return: |
| 331 | + """ |
| 332 | + self.update_view() |
| 333 | + self.list_view.adapter.data = data |
| 334 | + self.list_view.disabled = False |
| 335 | + self.list_view._trigger_reset_populate() |
| 336 | + |
| 337 | + def start_stop_simulation(self, simulate): |
| 338 | + """ |
| 339 | + Starts or stops simulating data |
| 340 | + :param simulate: |
| 341 | + :return: |
| 342 | + """ |
| 343 | + self.simulate = simulate |
| 344 | + |
| 345 | + if self.simulate: |
| 346 | + if self.dirty_thread: |
| 347 | + self.simulate_timer = BackgroundJob( |
| 348 | + self.time_interval, |
| 349 | + self._simulate_block_values |
| 350 | + ) |
| 351 | + self.simulate_timer.start() |
| 352 | + self.dirty_thread = False |
| 353 | + self.is_simulating = True |
| 354 | + else: |
| 355 | + self.simulate_timer.cancel() |
| 356 | + self.dirty_thread = True |
| 357 | + self.is_simulating = False |
| 358 | + |
| 359 | + def _simulate_block_values(self): |
| 360 | + if self.simulate: |
| 361 | + data = self.list_view.adapter.data |
| 362 | + if data: |
| 363 | + for index, value in data.items(): |
| 364 | + data[index] = randint(self.minval, self.maxval) |
| 365 | + print self.minval, self.maxval, data[index] |
| 366 | + self.refresh(data) |
| 367 | + self.dispatcher.dispatch('on_update', |
| 368 | + self._parent, |
| 369 | + self.blockname, |
| 370 | + self.list_view.adapter.data) |
| 371 | + |
| 372 | + |
| 373 | + |
| 374 | + |
0 commit comments