1""" 2Wrapper for the layout. 3""" 4from __future__ import unicode_literals 5 6import six 7 8from prompt_toolkit.buffer import Buffer 9 10from .containers import ConditionalContainer, Container, Window, to_container 11from .controls import BufferControl, UIControl 12 13__all__ = [ 14 'Layout', 15 'InvalidLayoutError', 16 'walk', 17] 18 19 20class Layout(object): 21 """ 22 The layout for a prompt_toolkit 23 :class:`~prompt_toolkit.application.Application`. 24 This also keeps track of which user control is focused. 25 26 :param container: The "root" container for the layout. 27 :param focused_element: element to be focused initially. (Can be anything 28 the `focus` function accepts.) 29 """ 30 def __init__(self, container, focused_element=None): 31 self.container = to_container(container) 32 self._stack = [] 33 34 # Map search BufferControl back to the original BufferControl. 35 # This is used to keep track of when exactly we are searching, and for 36 # applying the search. 37 # When a link exists in this dictionary, that means the search is 38 # currently active. 39 self.search_links = {} # search_buffer_control -> original buffer control. 40 41 # Mapping that maps the children in the layout to their parent. 42 # This relationship is calculated dynamically, each time when the UI 43 # is rendered. (UI elements have only references to their children.) 44 self._child_to_parent = {} 45 46 if focused_element is None: 47 try: 48 self._stack.append(next(self.find_all_windows())) 49 except StopIteration: 50 raise InvalidLayoutError('Invalid layout. The layout does not contain any Window object.') 51 else: 52 self.focus(focused_element) 53 54 # List of visible windows. 55 self.visible_windows = [] # List of `Window` objects. 56 57 def __repr__(self): 58 return 'Layout(%r, current_window=%r)' % ( 59 self.container, self.current_window) 60 61 def find_all_windows(self): 62 """ 63 Find all the :class:`.UIControl` objects in this layout. 64 """ 65 for item in self.walk(): 66 if isinstance(item, Window): 67 yield item 68 69 def find_all_controls(self): 70 for container in self.find_all_windows(): 71 yield container.content 72 73 def focus(self, value): 74 """ 75 Focus the given UI element. 76 77 `value` can be either: 78 79 - a :class:`.UIControl` 80 - a :class:`.Buffer` instance or the name of a :class:`.Buffer` 81 - a :class:`.Window` 82 - Any container object. In this case we will focus the :class:`.Window` 83 from this container that was focused most recent, or the very first 84 focusable :class:`.Window` of the container. 85 """ 86 # BufferControl by buffer name. 87 if isinstance(value, six.text_type): 88 for control in self.find_all_controls(): 89 if isinstance(control, BufferControl) and control.buffer.name == value: 90 self.focus(control) 91 return 92 raise ValueError("Couldn't find Buffer in the current layout: %r." % (value, )) 93 94 # BufferControl by buffer object. 95 elif isinstance(value, Buffer): 96 for control in self.find_all_controls(): 97 if isinstance(control, BufferControl) and control.buffer == value: 98 self.focus(control) 99 return 100 raise ValueError("Couldn't find Buffer in the current layout: %r." % (value, )) 101 102 # Focus UIControl. 103 elif isinstance(value, UIControl): 104 if value not in self.find_all_controls(): 105 raise ValueError('Invalid value. Container does not appear in the layout.') 106 if not value.is_focusable(): 107 raise ValueError('Invalid value. UIControl is not focusable.') 108 109 self.current_control = value 110 111 # Otherwise, expecting any Container object. 112 else: 113 value = to_container(value) 114 115 if isinstance(value, Window): 116 # This is a `Window`: focus that. 117 if value not in self.find_all_windows(): 118 raise ValueError('Invalid value. Window does not appear in the layout: %r' % 119 (value, )) 120 121 self.current_window = value 122 else: 123 # Focus a window in this container. 124 # If we have many windows as part of this container, and some 125 # of them have been focused before, take the last focused 126 # item. (This is very useful when the UI is composed of more 127 # complex sub components.) 128 windows = [] 129 for c in walk(value, skip_hidden=True): 130 if isinstance(c, Window) and c.content.is_focusable(): 131 windows.append(c) 132 133 # Take the first one that was focused before. 134 for w in reversed(self._stack): 135 if w in windows: 136 self.current_window = w 137 return 138 139 # None was focused before: take the very first focusable window. 140 if windows: 141 self.current_window = windows[0] 142 return 143 144 raise ValueError('Invalid value. Container cannot be focused: %r' % (value, )) 145 146 def has_focus(self, value): 147 """ 148 Check whether the given control has the focus. 149 :param value: :class:`.UIControl` or :class:`.Window` instance. 150 """ 151 if isinstance(value, six.text_type): 152 if self.current_buffer is None: 153 return False 154 return self.current_buffer.name == value 155 if isinstance(value, Buffer): 156 return self.current_buffer == value 157 if isinstance(value, UIControl): 158 return self.current_control == value 159 else: 160 value = to_container(value) 161 if isinstance(value, Window): 162 return self.current_window == value 163 else: 164 # Check whether this "container" is focused. This is true if 165 # one of the elements inside is focused. 166 for element in walk(value): 167 if element == self.current_window: 168 return True 169 return False 170 171 @property 172 def current_control(self): 173 """ 174 Get the :class:`.UIControl` to currently has the focus. 175 """ 176 return self._stack[-1].content 177 178 @current_control.setter 179 def current_control(self, control): 180 """ 181 Set the :class:`.UIControl` to receive the focus. 182 """ 183 assert isinstance(control, UIControl) 184 185 for window in self.find_all_windows(): 186 if window.content == control: 187 self.current_window = window 188 return 189 190 raise ValueError('Control not found in the user interface.') 191 192 @property 193 def current_window(self): 194 " Return the :class:`.Window` object that is currently focused. " 195 return self._stack[-1] 196 197 @current_window.setter 198 def current_window(self, value): 199 " Set the :class:`.Window` object to be currently focused. " 200 assert isinstance(value, Window) 201 self._stack.append(value) 202 203 @property 204 def is_searching(self): 205 " True if we are searching right now. " 206 return self.current_control in self.search_links 207 208 @property 209 def search_target_buffer_control(self): 210 " Return the :class:`.BufferControl` in which we are searching or `None`. " 211 return self.search_links.get(self.current_control) 212 213 def get_focusable_windows(self): 214 """ 215 Return all the :class:`.Window` objects which are focusable (in the 216 'modal' area). 217 """ 218 for w in self.walk_through_modal_area(): 219 if isinstance(w, Window) and w.content.is_focusable(): 220 yield w 221 222 def get_visible_focusable_windows(self): 223 """ 224 Return a list of :class:`.Window` objects that are focusable. 225 """ 226 # focusable windows are windows that are visible, but also part of the 227 # modal container. Make sure to keep the ordering. 228 visible_windows = self.visible_windows 229 return [w for w in self.get_focusable_windows() if w in visible_windows] 230 231 @property 232 def current_buffer(self): 233 """ 234 The currently focused :class:`~.Buffer` or `None`. 235 """ 236 ui_control = self.current_control 237 if isinstance(ui_control, BufferControl): 238 return ui_control.buffer 239 240 def get_buffer_by_name(self, buffer_name): 241 """ 242 Look in the layout for a buffer with the given name. 243 Return `None` when nothing was found. 244 """ 245 for w in self.walk(): 246 if isinstance(w, Window) and isinstance(w.content, BufferControl): 247 if w.content.buffer.name == buffer_name: 248 return w.content.buffer 249 250 @property 251 def buffer_has_focus(self): 252 """ 253 Return `True` if the currently focused control is a 254 :class:`.BufferControl`. (For instance, used to determine whether the 255 default key bindings should be active or not.) 256 """ 257 ui_control = self.current_control 258 return isinstance(ui_control, BufferControl) 259 260 @property 261 def previous_control(self): 262 """ 263 Get the :class:`.UIControl` to previously had the focus. 264 """ 265 try: 266 return self._stack[-2].content 267 except IndexError: 268 return self._stack[-1].content 269 270 def focus_last(self): 271 """ 272 Give the focus to the last focused control. 273 """ 274 if len(self._stack) > 1: 275 self._stack = self._stack[:-1] 276 277 def focus_next(self): 278 """ 279 Focus the next visible/focusable Window. 280 """ 281 windows = self.get_visible_focusable_windows() 282 283 if len(windows) > 0: 284 try: 285 index = windows.index(self.current_window) 286 except ValueError: 287 index = 0 288 else: 289 index = (index + 1) % len(windows) 290 291 self.focus(windows[index]) 292 293 def focus_previous(self): 294 """ 295 Focus the previous visible/focusable Window. 296 """ 297 windows = self.get_visible_focusable_windows() 298 299 if len(windows) > 0: 300 try: 301 index = windows.index(self.current_window) 302 except ValueError: 303 index = 0 304 else: 305 index = (index - 1) % len(windows) 306 307 self.focus(windows[index]) 308 309 def walk(self): 310 """ 311 Walk through all the layout nodes (and their children) and yield them. 312 """ 313 for i in walk(self.container): 314 yield i 315 316 def walk_through_modal_area(self): 317 """ 318 Walk through all the containers which are in the current 'modal' part 319 of the layout. 320 """ 321 # Go up in the tree, and find the root. (it will be a part of the 322 # layout, if the focus is in a modal part.) 323 root = self.current_window 324 while not root.is_modal() and root in self._child_to_parent: 325 root = self._child_to_parent[root] 326 327 for container in walk(root): 328 yield container 329 330 def update_parents_relations(self): 331 """ 332 Update child->parent relationships mapping. 333 """ 334 parents = {} 335 336 def walk(e): 337 for c in e.get_children(): 338 parents[c] = e 339 walk(c) 340 341 walk(self.container) 342 343 self._child_to_parent = parents 344 345 def reset(self): 346 # Remove all search links when the UI starts. 347 # (Important, for instance when control-c is been pressed while 348 # searching. The prompt cancels, but next `run()` call the search 349 # links are still there.) 350 self.search_links.clear() 351 352 return self.container.reset() 353 354 def get_parent(self, container): 355 """ 356 Return the parent container for the given container, or ``None``, if it 357 wasn't found. 358 """ 359 try: 360 return self._child_to_parent[container] 361 except KeyError: 362 return 363 364 365class InvalidLayoutError(Exception): 366 pass 367 368 369def walk(container, skip_hidden=False): 370 """ 371 Walk through layout, starting at this container. 372 """ 373 assert isinstance(container, Container) 374 375 # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers. 376 if skip_hidden and isinstance(container, ConditionalContainer) and not container.filter(): 377 return 378 379 yield container 380 381 for c in container.get_children(): 382 # yield from walk(c) 383 for i in walk(c, skip_hidden=skip_hidden): 384 yield i 385