1# Terminator by Chris Jones <cmsj@tenshu.net> 2# GPL v2 only 3"""window.py - class for the main Terminator window""" 4 5import copy 6import time 7import uuid 8import gi 9from gi.repository import GObject 10from gi.repository import Gtk, Gdk 11 12from .util import dbg, err, make_uuid, display_manager 13 14try: 15 from gi.repository import GdkX11 16except ImportError: 17 dbg("could not import X11 gir module") 18 19 20from . import util 21from .translation import _ 22from .version import APP_NAME 23from .container import Container 24from .factory import Factory 25from .terminator import Terminator 26if display_manager() == 'X11': 27 try: 28 gi.require_version('Keybinder', '3.0') 29 from gi.repository import Keybinder 30 Keybinder.init() 31 except (ImportError, ValueError): 32 err('Unable to load Keybinder module. This means the \ 33hide_window shortcut will be unavailable') 34 35# pylint: disable-msg=R0904 36class Window(Container, Gtk.Window): 37 """Class implementing a top-level Terminator window""" 38 39 terminator = None 40 title = None 41 isfullscreen = None 42 ismaximised = None 43 hidebound = None 44 hidefunc = None 45 losefocus_time = 0 46 position = None 47 ignore_startup_show = None 48 set_pos_by_ratio = None 49 last_active_term = None 50 preventHide = None 51 52 zoom_data = None 53 54 term_zoomed = False 55 __gproperties__ = { 56 'term_zoomed': (GObject.TYPE_BOOLEAN, 57 'terminal zoomed', 58 'whether the terminal is zoomed', 59 False, 60 GObject.PARAM_READWRITE) 61 } 62 63 def __init__(self): 64 """Class initialiser""" 65 self.terminator = Terminator() 66 self.terminator.register_window(self) 67 68 Container.__init__(self) 69 GObject.GObject.__init__(self) 70 GObject.type_register(Window) 71 self.register_signals(Window) 72 73 self.get_style_context().add_class("terminator-terminal-window") 74 75# self.set_property('allow-shrink', True) # FIXME FOR GTK3, or do we need this actually? 76 icon_to_apply='' 77 78 self.register_callbacks() 79 self.apply_config() 80 81 self.title = WindowTitle(self) 82 self.title.update() 83 84 self.preventHide = False 85 86 options = self.config.options_get() 87 if options: 88 if options.forcedtitle: 89 self.title.force_title(options.forcedtitle) 90 91 if options.role: 92 self.set_role(options.role) 93 94 if options.forcedicon is not None: 95 icon_to_apply = options.forcedicon 96 97 if options.geometry: 98 if not self.parse_geometry(options.geometry): 99 err('Window::__init__: Unable to parse geometry: %s' % 100 options.geometry) 101 102 self.apply_icon(icon_to_apply) 103 self.pending_set_rough_geometry_hint = False 104 105 def do_get_property(self, prop): 106 """Handle gobject getting a property""" 107 if prop.name in ['term_zoomed', 'term-zoomed']: 108 return(self.term_zoomed) 109 else: 110 raise AttributeError('unknown property %s' % prop.name) 111 112 def do_set_property(self, prop, value): 113 """Handle gobject setting a property""" 114 if prop.name in ['term_zoomed', 'term-zoomed']: 115 self.term_zoomed = value 116 else: 117 raise AttributeError('unknown property %s' % prop.name) 118 119 def register_callbacks(self): 120 """Connect the GTK+ signals we care about""" 121 self.connect('key-press-event', self.on_key_press) 122 self.connect('button-press-event', self.on_button_press) 123 self.connect('delete_event', self.on_delete_event) 124 self.connect('destroy', self.on_destroy_event) 125 self.connect('window-state-event', self.on_window_state_changed) 126 self.connect('focus-out-event', self.on_focus_out) 127 self.connect('focus-in-event', self.on_focus_in) 128 129 # Attempt to grab a global hotkey for hiding the window. 130 # If we fail, we'll never hide the window, iconifying instead. 131 if self.config['keybindings']['hide_window'] != None: 132 if display_manager() == 'X11': 133 try: 134 self.hidebound = Keybinder.bind( 135 self.config['keybindings']['hide_window'].replace('<Shift>',''), 136 self.on_hide_window) 137 except (KeyError, NameError): 138 pass 139 140 if not self.hidebound: 141 err('Unable to bind hide_window key, another instance/window has it.') 142 self.hidefunc = self.iconify 143 else: 144 self.hidefunc = self.hide 145 146 def apply_config(self): 147 """Apply various configuration options""" 148 options = self.config.options_get() 149 maximise = self.config['window_state'] == 'maximise' 150 fullscreen = self.config['window_state'] == 'fullscreen' 151 hidden = self.config['window_state'] == 'hidden' 152 borderless = self.config['borderless'] 153 skiptaskbar = self.config['hide_from_taskbar'] 154 alwaysontop = self.config['always_on_top'] 155 sticky = self.config['sticky'] 156 157 if options: 158 if options.maximise: 159 maximise = True 160 if options.fullscreen: 161 fullscreen = True 162 if options.hidden: 163 hidden = True 164 if options.borderless: 165 borderless = True 166 167 self.set_fullscreen(fullscreen) 168 self.set_maximised(maximise) 169 self.set_borderless(borderless) 170 self.set_always_on_top(alwaysontop) 171 self.set_real_transparency() 172 self.set_sticky(sticky) 173 if self.hidebound: 174 self.set_hidden(hidden) 175 self.set_skip_taskbar_hint(skiptaskbar) 176 else: 177 self.set_iconified(hidden) 178 179 def apply_icon(self, requested_icon): 180 """Set the window icon""" 181 icon_theme = Gtk.IconTheme.get_default() 182 icon_name_list = [APP_NAME] # disable self.wmclass_name, n/a in GTK3 183 184 if requested_icon: 185 try: 186 self.set_icon_from_file(requested_icon) 187 return 188 except (NameError, GObject.GError): 189 dbg('Unable to load %s icon as file' % (repr(requested_icon))) 190 191 icon_name_list.insert(0, requested_icon) 192 193 for icon_name in icon_name_list: 194 # Test if the icon is available first 195 if icon_theme.lookup_icon(icon_name, 48, 0): 196 self.set_icon_name(icon_name) 197 return # Success! We're done. 198 else: 199 dbg('Unable to load %s icon' % (icon_name)) 200 201 icon = self.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON) 202 self.set_icon(icon) 203 204 def on_key_press(self, window, event): 205 """Handle a keyboard event""" 206 maker = Factory() 207 208 self.set_urgency_hint(False) 209 210 mapping = self.terminator.keybindings.lookup(event) 211 212 if mapping: 213 dbg('Window::on_key_press: looked up %r' % mapping) 214 if mapping == 'full_screen': 215 self.set_fullscreen(not self.isfullscreen) 216 elif mapping == 'close_window': 217 if not self.on_delete_event(window, 218 Gdk.Event.new(Gdk.EventType.DELETE)): 219 self.on_destroy_event(window, 220 Gdk.Event.new(Gdk.EventType.DESTROY)) 221 else: 222 return(False) 223 return(True) 224 225 def on_button_press(self, window, event): 226 """Handle a mouse button event. Mainly this is just a clean way to 227 cancel any urgency hints that are set.""" 228 self.set_urgency_hint(False) 229 return(False) 230 231 def on_focus_out(self, window, event): 232 """Focus has left the window""" 233 for terminal in self.get_visible_terminals(): 234 terminal.on_window_focus_out() 235 236 self.losefocus_time = time.time() 237 238 if self.preventHide: 239 self.preventHide = False 240 else: 241 if self.config['hide_on_lose_focus'] and self.get_property('visible'): 242 self.position = self.get_position() 243 self.hidefunc() 244 245 def on_focus_in(self, window, event): 246 """Focus has entered the window""" 247 self.set_urgency_hint(False) 248 if not self.terminator.doing_layout: 249 self.terminator.last_active_window = self.uuid 250 # FIXME: Cause the terminal titlebars to update here 251 252 def is_child_notebook(self): 253 """Returns True if this Window's child is a Notebook""" 254 maker = Factory() 255 return(maker.isinstance(self.get_child(), 'Notebook')) 256 257 def tab_new(self, widget=None, debugtab=False, _param1=None, _param2=None): 258 """Make a new tab""" 259 cwd = None 260 profile = None 261 262 if self.get_property('term_zoomed') == True: 263 err("You can't create a tab while a terminal is maximised/zoomed") 264 return 265 266 if widget: 267 cwd = widget.get_cwd() 268 profile = widget.get_profile() 269 270 maker = Factory() 271 if not self.is_child_notebook(): 272 dbg('Making a new Notebook') 273 notebook = maker.make('Notebook', window=self) 274 self.show() 275 self.present() 276 return self.get_child().newtab(debugtab, cwd=cwd, profile=profile) 277 278 def on_delete_event(self, window, event, data=None): 279 """Handle a window close request""" 280 maker = Factory() 281 if maker.isinstance(self.get_child(), 'Terminal'): 282 if self.get_property('term_zoomed') == True: 283 return(self.confirm_close(window, _('window'))) 284 else: 285 dbg('Window::on_delete_event: Only one child, closing is fine') 286 return(False) 287 elif maker.isinstance(self.get_child(), 'Container'): 288 return(self.confirm_close(window, _('window'))) 289 else: 290 dbg('unknown child: %s' % self.get_child()) 291 292 def confirm_close(self, window, type): 293 """Display a confirmation dialog when the user is closing multiple 294 terminals in one window""" 295 296 return(not (self.construct_confirm_close(window, type) == Gtk.ResponseType.ACCEPT)) 297 298 def on_destroy_event(self, widget, data=None): 299 """Handle window destruction""" 300 dbg('destroying self') 301 for terminal in self.get_visible_terminals(): 302 terminal.close() 303 self.cnxids.remove_all() 304 self.terminator.deregister_window(self) 305 self.destroy() 306 del(self) 307 308 def on_hide_window(self, data=None): 309 """Handle a request to hide/show the window""" 310 311 if not self.get_property('visible'): 312 #Don't show if window has just been hidden because of 313 #lost focus 314 if (time.time() - self.losefocus_time < 0.1) and \ 315 self.config['hide_on_lose_focus']: 316 return 317 if self.position: 318 self.move(self.position[0], self.position[1]) 319 self.show() 320 self.grab_focus() 321 try: 322 t = GdkX11.x11_get_server_time(self.get_window()) 323 except (TypeError, AttributeError): 324 t = 0 325 self.get_window().focus(t) 326 else: 327 self.position = self.get_position() 328 self.hidefunc() 329 330 # pylint: disable-msg=W0613 331 def on_window_state_changed(self, window, event): 332 """Handle the state of the window changing""" 333 self.isfullscreen = bool(event.new_window_state & 334 Gdk.WindowState.FULLSCREEN) 335 self.ismaximised = bool(event.new_window_state & 336 Gdk.WindowState.MAXIMIZED) 337 dbg('Window::on_window_state_changed: fullscreen=%s, maximised=%s' \ 338 % (self.isfullscreen, self.ismaximised)) 339 340 return(False) 341 342 def set_maximised(self, value): 343 """Set the maximised state of the window from the supplied value""" 344 if value == True: 345 self.maximize() 346 else: 347 self.unmaximize() 348 349 def set_fullscreen(self, value): 350 """Set the fullscreen state of the window from the supplied value""" 351 if value == True: 352 self.fullscreen() 353 else: 354 self.unfullscreen() 355 356 def set_borderless(self, value): 357 """Set the state of the window border from the supplied value""" 358 self.set_decorated (not value) 359 360 def set_hidden(self, value): 361 """Set the visibility of the window from the supplied value""" 362 if value == True: 363 self.ignore_startup_show = True 364 else: 365 self.ignore_startup_show = False 366 367 def set_iconified(self, value): 368 """Set the minimised state of the window from the supplied value""" 369 if value == True: 370 self.iconify() 371 372 def set_always_on_top(self, value): 373 """Set the always on top window hint from the supplied value""" 374 self.set_keep_above(value) 375 376 def set_sticky(self, value): 377 """Set the sticky hint from the supplied value""" 378 if value == True: 379 self.stick() 380 381 def set_real_transparency(self, value=True): 382 """Enable RGBA if supported on the current screen""" 383 if self.is_composited() == False: 384 value = False 385 386 screen = self.get_screen() 387 if value: 388 dbg('setting rgba visual') 389 visual = screen.get_rgba_visual() 390 if visual: 391 self.set_visual(visual) 392 393 def show(self, startup=False): 394 """Undo the startup show request if started in hidden mode""" 395 #Present is necessary to grab focus when window is hidden from taskbar. 396 #It is important to call present() before show(), otherwise the window 397 #won't be brought to front if an another application has the focus. 398 #Last note: present() will implicitly call Gtk.Window.show() 399 self.present() 400 401 #Window must be shown, then hidden for the hotkeys to be registered 402 if (self.ignore_startup_show and startup == True): 403 self.position = self.get_position() 404 self.hide() 405 406 407 def add(self, widget, metadata=None): 408 """Add a widget to the window by way of Gtk.Window.add()""" 409 maker = Factory() 410 Gtk.Window.add(self, widget) 411 if maker.isinstance(widget, 'Terminal'): 412 signals = {'close-term': self.closeterm, 413 'title-change': self.title.set_title, 414 'split-horiz': self.split_horiz, 415 'split-vert': self.split_vert, 416 'unzoom': self.unzoom, 417 'tab-change': self.tab_change, 418 'group-all': self.group_all, 419 'group-all-toggle': self.group_all_toggle, 420 'ungroup-all': self.ungroup_all, 421 'group-tab': self.group_tab, 422 'group-tab-toggle': self.group_tab_toggle, 423 'ungroup-tab': self.ungroup_tab, 424 'move-tab': self.move_tab, 425 'tab-new': [self.tab_new, widget], 426 'navigate': self.navigate_terminal} 427 428 for signal in signals: 429 args = [] 430 handler = signals[signal] 431 if isinstance(handler, list): 432 args = handler[1:] 433 handler = handler[0] 434 self.connect_child(widget, signal, handler, *args) 435 436 widget.grab_focus() 437 438 def remove(self, widget): 439 """Remove our child widget by way of Gtk.Window.remove()""" 440 Gtk.Window.remove(self, widget) 441 self.disconnect_child(widget) 442 return(True) 443 444 def get_children(self): 445 """Return a single list of our child""" 446 children = [] 447 children.append(self.get_child()) 448 return(children) 449 450 def hoover(self): 451 """Ensure we still have a reason to exist""" 452 if not self.get_child(): 453 self.emit('destroy') 454 455 def closeterm(self, widget): 456 """Handle a terminal closing""" 457 Container.closeterm(self, widget) 458 self.hoover() 459 460 def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst=True): 461 """Split the window""" 462 if self.get_property('term_zoomed') == True: 463 err("You can't split while a terminal is maximised/zoomed") 464 return 465 466 order = None 467 maker = Factory() 468 self.remove(widget) 469 470 if vertical: 471 container = maker.make('VPaned') 472 else: 473 container = maker.make('HPaned') 474 475 self.set_pos_by_ratio = True 476 477 if not sibling: 478 sibling = maker.make('Terminal') 479 sibling.set_cwd(cwd) 480 if self.config['always_split_with_profile']: 481 sibling.force_set_profile(None, widget.get_profile()) 482 sibling.spawn_child() 483 if widget.group and self.config['split_to_group']: 484 sibling.set_group(None, widget.group) 485 elif self.config['always_split_with_profile']: 486 sibling.force_set_profile(None, widget.get_profile()) 487 488 self.add(container) 489 container.show_all() 490 491 order = [widget, sibling] 492 if widgetfirst is False: 493 order.reverse() 494 495 for term in order: 496 container.add(term) 497 container.show_all() 498 499 while Gtk.events_pending(): 500 Gtk.main_iteration_do(False) 501 sibling.grab_focus() 502 self.set_pos_by_ratio = False 503 504 505 def zoom(self, widget, font_scale=True): 506 """Zoom a terminal widget""" 507 children = self.get_children() 508 509 if widget in children: 510 # This widget is a direct child of ours and we're a Window 511 # so zooming is a no-op 512 return 513 514 self.zoom_data = widget.get_zoom_data() 515 self.zoom_data['widget'] = widget 516 self.zoom_data['old_child'] = children[0] 517 self.zoom_data['font_scale'] = font_scale 518 519 self.remove(self.zoom_data['old_child']) 520 self.zoom_data['old_parent'].remove(widget) 521 self.add(widget) 522 self.set_property('term_zoomed', True) 523 524 if font_scale: 525 widget.cnxids.new(widget, 'size-allocate', 526 widget.zoom_scale, self.zoom_data) 527 528 widget.grab_focus() 529 530 def unzoom(self, widget): 531 """Restore normal terminal layout""" 532 if not self.get_property('term_zoomed'): 533 # We're not zoomed anyway 534 dbg('Window::unzoom: not zoomed, no-op') 535 return 536 537 widget = self.zoom_data['widget'] 538 if self.zoom_data['font_scale']: 539 widget.vte.set_font(self.zoom_data['old_font']) 540 541 self.remove(widget) 542 self.add(self.zoom_data['old_child']) 543 self.zoom_data['old_parent'].add(widget) 544 widget.grab_focus() 545 self.zoom_data = None 546 self.set_property('term_zoomed', False) 547 548 def rotate(self, widget, clockwise): 549 """Rotate children in this window""" 550 self.set_pos_by_ratio = True 551 maker = Factory() 552 child = self.get_child() 553 554 # If our child is a Notebook, reset to work from its visible child 555 if maker.isinstance(child, 'Notebook'): 556 pagenum = child.get_current_page() 557 child = child.get_nth_page(pagenum) 558 559 if maker.isinstance(child, 'Paned'): 560 parent = child.get_parent() 561 # Need to get the allocation before we remove the child, 562 # otherwise _sometimes_ we get incorrect values. 563 alloc = child.get_allocation() 564 parent.remove(child) 565 child.rotate_recursive(parent, alloc.width, alloc.height, clockwise) 566 567 self.show_all() 568 while Gtk.events_pending(): 569 Gtk.main_iteration_do(False) 570 widget.grab_focus() 571 572 self.set_pos_by_ratio = False 573 574 def get_visible_terminals(self): 575 """Walk down the widget tree to find all of the visible terminals. 576 Mostly using Container::get_visible_terminals()""" 577 terminals = {} 578 if not hasattr(self, 'cached_maker'): 579 self.cached_maker = Factory() 580 maker = self.cached_maker 581 child = self.get_child() 582 583 if not child: 584 return([]) 585 586 # If our child is a Notebook, reset to work from its visible child 587 if maker.isinstance(child, 'Notebook'): 588 pagenum = child.get_current_page() 589 child = child.get_nth_page(pagenum) 590 591 if maker.isinstance(child, 'Container'): 592 terminals.update(child.get_visible_terminals()) 593 elif maker.isinstance(child, 'Terminal'): 594 terminals[child] = child.get_allocation() 595 else: 596 err('Unknown child type %s' % type(child)) 597 598 return(terminals) 599 600 def get_focussed_terminal(self): 601 """Find which terminal we want to have focus""" 602 terminals = self.get_visible_terminals() 603 for terminal in terminals: 604 if terminal.vte.is_focus(): 605 return(terminal) 606 return(None) 607 608 def deferred_set_rough_geometry_hints(self): 609 # no parameters are used in set_rough_geometry_hints, so we can 610 # use the set_rough_geometry_hints 611 if self.pending_set_rough_geometry_hint == True: 612 return 613 self.pending_set_rough_geometry_hint = True 614 GObject.idle_add(self.do_deferred_set_rough_geometry_hints) 615 616 def do_deferred_set_rough_geometry_hints(self): 617 self.pending_set_rough_geometry_hint = False 618 self.set_rough_geometry_hints() 619 620 def set_rough_geometry_hints(self): 621 """Walk all the terminals along the top and left edges to fake up how 622 many columns/rows we sort of have""" 623 if self.ismaximised == True: 624 return 625 if not hasattr(self, 'cached_maker'): 626 self.cached_maker = Factory() 627 maker = self.cached_maker 628 if maker.isinstance(self.get_child(), 'Notebook'): 629 dbg("We don't currently support geometry hinting with tabs") 630 return 631 632 terminals = self.get_visible_terminals() 633 column_sum = 0 634 row_sum = 0 635 636 for terminal in terminals: 637 rect = terminal.get_allocation() 638 if rect.x == 0: 639 cols, rows = terminal.get_size() 640 row_sum = row_sum + rows 641 if rect.y == 0: 642 cols, rows = terminal.get_size() 643 column_sum = column_sum + cols 644 645 if column_sum == 0 or row_sum == 0: 646 dbg('column_sum=%s,row_sum=%s. No terminals found in >=1 axis' % 647 (column_sum, row_sum)) 648 return 649 650 # FIXME: I don't think we should just use whatever font size info is on 651 # the last terminal we inspected. Looking up the default profile font 652 # size and calculating its character sizes would be rather expensive 653 # though. 654 font_width, font_height = terminal.get_font_size() 655 total_font_width = font_width * column_sum 656 total_font_height = font_height * row_sum 657 658 win_width, win_height = self.get_size() 659 extra_width = win_width - total_font_width 660 extra_height = win_height - total_font_height 661 662 dbg('setting geometry hints: (ewidth:%s)(eheight:%s),\ 663(fwidth:%s)(fheight:%s)' % (extra_width, extra_height, 664 font_width, font_height)) 665 geometry = Gdk.Geometry() 666 geometry.base_width = extra_width 667 geometry.base_height = extra_height 668 geometry.width_inc = font_width 669 geometry.height_inc = font_height 670 self.set_geometry_hints(self, geometry, Gdk.WindowHints.BASE_SIZE | Gdk.WindowHints.RESIZE_INC) 671 672 def tab_change(self, widget, num=None): 673 """Change to a specific tab""" 674 if num is None: 675 err('must specify a tab to change to') 676 677 maker = Factory() 678 child = self.get_child() 679 680 if not maker.isinstance(child, 'Notebook'): 681 dbg('child is not a notebook, nothing to change to') 682 return 683 684 if num == -1: 685 # Go to the next tab 686 cur = child.get_current_page() 687 pages = child.get_n_pages() 688 if cur == pages - 1: 689 num = 0 690 else: 691 num = cur + 1 692 elif num == -2: 693 # Go to the previous tab 694 cur = child.get_current_page() 695 if cur > 0: 696 num = cur - 1 697 else: 698 num = child.get_n_pages() - 1 699 700 child.set_current_page(num) 701 # Work around strange bug in gtk-2.12.11 and pygtk-2.12.1 702 # Without it, the selection changes, but the displayed page doesn't 703 # change 704 child.set_current_page(child.get_current_page()) 705 706 def set_groups(self, new_group, term_list): 707 """Set terminals in term_list to new_group""" 708 for terminal in term_list: 709 terminal.set_group(None, new_group) 710 self.terminator.focus_changed(self.terminator.last_focused_term) 711 712 def group_all(self, widget): 713 """Group all terminals""" 714 # FIXME: Why isn't this being done by Terminator() ? 715 group = _('All') 716 self.terminator.create_group(group) 717 self.set_groups(group, self.terminator.terminals) 718 719 def group_all_toggle(self, widget): 720 """Toggle grouping to all""" 721 if widget.group == 'All': 722 self.ungroup_all(widget) 723 else: 724 self.group_all(widget) 725 726 def ungroup_all(self, widget): 727 """Ungroup all terminals""" 728 self.set_groups(None, self.terminator.terminals) 729 730 def group_tab(self, widget): 731 """Group all terminals in the current tab""" 732 maker = Factory() 733 notebook = self.get_child() 734 735 if not maker.isinstance(notebook, 'Notebook'): 736 dbg('not in a notebook, refusing to group tab') 737 return 738 739 pagenum = notebook.get_current_page() 740 while True: 741 group = _('Tab %d') % pagenum 742 if group not in self.terminator.groups: 743 break 744 pagenum += 1 745 self.set_groups(group, self.get_visible_terminals()) 746 747 def group_tab_toggle(self, widget): 748 """Blah""" 749 if widget.group and widget.group[:4] == 'Tab ': 750 self.ungroup_tab(widget) 751 else: 752 self.group_tab(widget) 753 754 def ungroup_tab(self, widget): 755 """Ungroup all terminals in the current tab""" 756 maker = Factory() 757 notebook = self.get_child() 758 759 if not maker.isinstance(notebook, 'Notebook'): 760 dbg('note in a notebook, refusing to ungroup tab') 761 return 762 763 self.set_groups(None, self.get_visible_terminals()) 764 765 def move_tab(self, widget, direction): 766 """Handle a keyboard shortcut for moving tab positions""" 767 maker = Factory() 768 notebook = self.get_child() 769 770 if not maker.isinstance(notebook, 'Notebook'): 771 dbg('not in a notebook, refusing to move tab %s' % direction) 772 return 773 774 dbg('moving tab %s' % direction) 775 numpages = notebook.get_n_pages() 776 page = notebook.get_current_page() 777 child = notebook.get_nth_page(page) 778 779 if direction == 'left': 780 if page == 0: 781 page = numpages 782 else: 783 page = page - 1 784 elif direction == 'right': 785 if page == numpages - 1: 786 page = 0 787 else: 788 page = page + 1 789 else: 790 err('unknown direction: %s' % direction) 791 return 792 793 notebook.reorder_child(child, page) 794 795 def navigate_terminal(self, terminal, direction): 796 """Navigate around terminals""" 797 _containers, terminals = util.enumerate_descendants(self) 798 visibles = self.get_visible_terminals() 799 current = terminals.index(terminal) 800 length = len(terminals) 801 next = None 802 803 if length <= 1 or len(visibles) <= 1: 804 return 805 806 if direction in ['next', 'prev']: 807 tmpterms = copy.copy(terminals) 808 tmpterms = tmpterms[current+1:] 809 tmpterms.extend(terminals[0:current]) 810 811 if direction == 'next': 812 tmpterms.reverse() 813 814 next = 0 815 while len(tmpterms) > 0: 816 tmpitem = tmpterms.pop() 817 if tmpitem in visibles: 818 next = terminals.index(tmpitem) 819 break 820 elif direction in ['left', 'right', 'up', 'down']: 821 layout = self.get_visible_terminals() 822 allocation = terminal.get_allocation() 823 possibles = [] 824 825 # Get the co-ordinate of the appropriate edge for this direction 826 edge, p1, p2 = util.get_edge(allocation, direction) 827 # Find all visible terminals which are, in their entirity, in the 828 # direction we want to move, and are at least partially spanning 829 # p1 to p2 830 for term in layout: 831 rect = layout[term] 832 if util.get_nav_possible(edge, rect, direction, p1, p2): 833 possibles.append(term) 834 835 if len(possibles) == 0: 836 return 837 838 # Find out how far away each of the possible terminals is, then 839 # find the smallest distance. The winning terminals are all of 840 # those who are that distance away. 841 offsets = {} 842 for term in possibles: 843 rect = layout[term] 844 offsets[term] = util.get_nav_offset(edge, rect, direction) 845 keys = list(offsets.values()) 846 keys.sort() 847 winners = [k for k, v in offsets.items() if v == keys[0]] 848 next = terminals.index(winners[0]) 849 850 if len(winners) > 1: 851 # Break an n-way tie using the cursor position 852 term_alloc = terminal.get_allocation() 853 cursor_x = term_alloc.x + term_alloc.width / 2 854 cursor_y = term_alloc.y + term_alloc.height / 2 855 856 for term in winners: 857 rect = layout[term] 858 if util.get_nav_tiebreak(direction, cursor_x, cursor_y, 859 rect): 860 next = terminals.index(term) 861 break; 862 else: 863 err('Unknown navigation direction: %s' % direction) 864 865 if next is not None: 866 terminals[next].grab_focus() 867 868 def create_layout(self, layout): 869 """Apply any config items from our layout""" 870 if 'children' not in layout: 871 err('layout describes no children: %s' % layout) 872 return 873 children = layout['children'] 874 if len(children) != 1: 875 # We're a Window, we can only have one child 876 err('incorrect number of children for Window: %s' % layout) 877 return 878 879 child = children[list(children.keys())[0]] 880 terminal = self.get_children()[0] 881 dbg('Making a child of type: %s' % child['type']) 882 if child['type'] == 'VPaned': 883 self.split_axis(terminal, True) 884 elif child['type'] == 'HPaned': 885 self.split_axis(terminal, False) 886 elif child['type'] == 'Notebook': 887 self.tab_new() 888 i = 2 889 while i < len(child['children']): 890 self.tab_new() 891 i = i + 1 892 elif child['type'] == 'Terminal': 893 pass 894 else: 895 err('unknown child type: %s' % child['type']) 896 return 897 898 self.get_children()[0].create_layout(child) 899 900 if 'last_active_term' in layout and layout['last_active_term'] not in ['', None]: 901 self.last_active_term = make_uuid(layout['last_active_term']) 902 903 if 'last_active_window' in layout and layout['last_active_window'] == 'True': 904 self.terminator.last_active_window = self.uuid 905 906class WindowTitle(object): 907 """Class to handle the setting of the window title""" 908 909 window = None 910 text = None 911 forced = None 912 913 def __init__(self, window): 914 """Class initialiser""" 915 self.window = window 916 self.forced = False 917 918 def set_title(self, widget, text): 919 """Set the title""" 920 if not self.forced: 921 self.text = text 922 self.update() 923 924 def force_title(self, newtext): 925 """Force a specific title""" 926 if newtext: 927 self.set_title(None, newtext) 928 self.forced = True 929 else: 930 self.forced = False 931 932 def update(self): 933 """Update the title automatically""" 934 title = None 935 936 # FIXME: What the hell is this for?! 937 if self.forced: 938 title = self.text 939 else: 940 title = "%s" % self.text 941 942 self.window.set_title(title) 943 944# vim: set expandtab ts=4 sw=4: 945