def enumerate_descendants(parent): """Walk all our children and build up a list of containers and terminals""" # FIXME: Does having to import this here mean we should move this function # back to Container? from terminatorlib.factory import Factory containerstmp = [] containers = [] terminals = [] maker = Factory() if parent is None: err('no parent widget specified') return for descendant in parent.get_children(): if maker.isinstance(descendant, 'Container'): containerstmp.append(descendant) elif maker.isinstance(descendant, 'Terminal'): terminals.append(descendant) while len(containerstmp) > 0: child = containerstmp.pop(0) for descendant in child.get_children(): if maker.isinstance(descendant, 'Container'): containerstmp.append(descendant) elif maker.isinstance(descendant, 'Terminal'): terminals.append(descendant) containers.append(child) dbg(f'{len(containers)} containers and {len(terminals)} terminals fall beneath {parent}' ) return containers, terminals
def rotate(self, widget, clockwise): """Rotate children in this window""" self.set_pos_by_ratio = True maker = Factory() child = self.get_child() # If our child is a Notebook, reset to work from its visible child if maker.isinstance(child, 'Notebook'): pagenum = child.get_current_page() child = child.get_nth_page(pagenum) if maker.isinstance(child, 'Paned'): parent = child.get_parent() # Need to get the allocation before we remove the child, # otherwise _sometimes_ we get incorrect values. alloc = child.get_allocation() parent.remove(child) child.rotate_recursive(parent, alloc.width, alloc.height, clockwise) self.show_all() while Gtk.events_pending(): Gtk.main_iteration_do(False) widget.grab_focus() self.set_pos_by_ratio = False
def get_tab_title(self, uuid=None): """Return the title of a parent tab of a given terminal""" maker = Factory() terminal = self.terminator.find_terminal_by_uuid(uuid) window = terminal.get_toplevel() root_widget = window.get_children()[0] if maker.isinstance(root_widget, "Notebook"): for tab_child in root_widget.get_children(): terms = [tab_child] if not maker.isinstance(terms[0], "Terminal"): terms = enumerate_descendants(tab_child)[1] if terminal in terms: return root_widget.get_tab_label(tab_child).get_label()
def on_delete_event(self, window, event, data=None): """Handle a window close request""" maker = Factory() if maker.isinstance(self.get_child(), 'Terminal'): if self.get_property('term_zoomed') == True: return self.confirm_close(window, _('window')) else: dbg('Window::on_delete_event: Only one child, closing is fine') return False elif maker.isinstance(self.get_child(), 'Container'): return self.confirm_close(window, _('window')) else: dbg('unknown child: %s' % self.get_child())
def propagate_title_change(self, widget, title): """Pass a title change up the widget stack""" maker = Factory() parent = self.get_parent() title = widget.get_window_title() if maker.isinstance(self, 'Notebook'): self.update_tab_label_text(widget, title) elif maker.isinstance(self, 'Window'): self.title.set_title(widget, title) if maker.isinstance(parent, 'Container'): parent.propagate_title_change(widget, title)
def closetab(self, widget, label): """Close a tab""" tabnum = None try: nb = widget.notebook except AttributeError: err('TabLabel::closetab: called on non-Notebook: %s' % widget) return for i in range(0, nb.get_n_pages() + 1): if label == nb.get_tab_label(nb.get_nth_page(i)): tabnum = i break if tabnum is None: err('TabLabel::closetab: %s not in %s. Bailing.' % (label, nb)) return maker = Factory() child = nb.get_nth_page(tabnum) if maker.isinstance(child, 'Terminal'): dbg('Notebook::closetab: child is a single Terminal') del nb.last_active_term[child] child.close() # FIXME: We only do this del and return here to avoid removing the # page below, which child.close() implicitly does del(label) return elif maker.isinstance(child, 'Container'): dbg('Notebook::closetab: child is a Container') result = self.construct_confirm_close(self.window, _('tab')) if result == Gtk.ResponseType.ACCEPT: containers = None objects = None containers, objects = enumerate_descendants(child) while len(objects) > 0: descendant = objects.pop() descendant.close() while Gtk.events_pending(): Gtk.main_iteration() return else: dbg('Notebook::closetab: user cancelled request') return else: err('Notebook::closetab: child is unknown type %s' % child) return
def move_tab(self, widget, direction): """Handle a keyboard shortcut for moving tab positions""" maker = Factory() notebook = self.get_child() if not maker.isinstance(notebook, 'Notebook'): dbg('not in a notebook, refusing to move tab %s' % direction) return dbg('moving tab %s' % direction) numpages = notebook.get_n_pages() page = notebook.get_current_page() child = notebook.get_nth_page(page) if direction == 'left': if page == 0: page = numpages else: page = page - 1 elif direction == 'right': if page == numpages - 1: page = 0 else: page = page + 1 else: err('unknown direction: %s' % direction) return notebook.reorder_child(child, page)
def tab_change(self, widget, num=None): """Change to a specific tab""" if num is None: err('must specify a tab to change to') maker = Factory() child = self.get_child() if not maker.isinstance(child, 'Notebook'): dbg('child is not a notebook, nothing to change to') return if num == -1: # Go to the next tab cur = child.get_current_page() pages = child.get_n_pages() if cur == pages - 1: num = 0 else: num = cur + 1 elif num == -2: # Go to the previous tab cur = child.get_current_page() if cur > 0: num = cur - 1 else: num = child.get_n_pages() - 1 child.set_current_page(num) # Work around strange bug in gtk-2.12.11 and pygtk-2.12.1 # Without it, the selection changes, but the displayed page doesn't # change child.set_current_page(child.get_current_page())
def get_tab(self, uuid=None): """Return the UUID of the parent tab of a given terminal""" maker = Factory() terminal = self.terminator.find_terminal_by_uuid(uuid) window = terminal.get_toplevel() root_widget = window.get_children()[0] if maker.isinstance(root_widget, 'Notebook'): #return root_widget.uuid.urn for tab_child in root_widget.get_children(): terms = [tab_child] if not maker.isinstance(terms[0], "Terminal"): terms = enumerate_descendants(tab_child)[1] if terminal in terms: # FIXME: There are no uuid's assigned to the the notebook, or the actual tabs! # This would fail: return root_widget.uuid.urn return ""
def add(self, widget, metadata=None): """Add a widget to the window by way of Gtk.Window.add()""" maker = Factory() Gtk.Window.add(self, widget) if maker.isinstance(widget, 'Terminal'): signals = { 'close-term': self.closeterm, 'title-change': self.title.set_title, 'split-horiz': self.split_horiz, 'split-vert': self.split_vert, 'unzoom': self.unzoom, 'tab-change': self.tab_change, 'group-all': self.group_all, 'group-all-toggle': self.group_all_toggle, 'ungroup-all': self.ungroup_all, 'group-tab': self.group_tab, 'group-tab-toggle': self.group_tab_toggle, 'ungroup-tab': self.ungroup_tab, 'move-tab': self.move_tab, 'tab-new': [self.tab_new, widget], 'navigate': self.navigate_terminal } for signal in signals: args = [] handler = signals[signal] if isinstance(handler, list): args = handler[1:] handler = handler[0] self.connect_child(widget, signal, handler, *args) widget.grab_focus()
def rotate_recursive(self, parent, w, h, clockwise): """ Recursively rotate "self" into a new paned that'll have "w" x "h" size. Attach it to "parent". As discussed in LP#1522542, we should build up the new layout (including the separator positions) in a single step. We can't rely on Gtk+ computing the allocation sizes yet, so we have to do the computation ourselves and carry the resulting paned sizes all the way down the widget tree. """ maker = Factory() handle_size = self.get_handlesize() if isinstance(self, HPaned): container = VPaned() reverse = not clockwise else: container = HPaned() reverse = clockwise container.ratio = self.ratio children = self.get_children() if reverse: container.ratio = 1 - container.ratio children.reverse() if isinstance(self, HPaned): w1 = w2 = w h1 = pos = self.position_by_ratio(h, handle_size, container.ratio) h2 = max(h - h1 - handle_size, 0) else: h1 = h2 = h w1 = pos = self.position_by_ratio(w, handle_size, container.ratio) w2 = max(w - w1 - handle_size, 0) container.set_pos(pos) parent.add(container) if maker.isinstance(children[0], 'Terminal'): children[0].get_parent().remove(children[0]) container.add(children[0]) else: children[0].rotate_recursive(container, w1, h1, clockwise) if maker.isinstance(children[1], 'Terminal'): children[1].get_parent().remove(children[1]) container.add(children[1]) else: children[1].rotate_recursive(container, w2, h2, clockwise)
def ungroup_tab(self, widget): """Ungroup all terminals in the current tab""" maker = Factory() notebook = self.get_child() if not maker.isinstance(notebook, 'Notebook'): dbg('note in a notebook, refusing to ungroup tab') return self.set_groups(None, self.get_visible_terminals())
def do_redistribute(self, recurse_up=False, recurse_down=False): """Evenly divide available space between sibling panes""" maker = Factory() # 1 Find highest ancestor of the same type => ha highest_ancestor = self while isinstance(highest_ancestor.get_parent(), type(highest_ancestor)): highest_ancestor = highest_ancestor.get_parent() highest_ancestor.set_autoresize(False) # (1b) If Super modifier, redistribute higher sections too if recurse_up: grandfather = highest_ancestor.get_parent() if maker.isinstance(grandfather, 'VPaned') or \ maker.isinstance(grandfather, 'HPaned'): grandfather.do_redistribute(recurse_up, recurse_down) highest_ancestor._do_redistribute(recurse_up, recurse_down) GLib.idle_add(highest_ancestor.set_autoresize, True)
def _do_redistribute(self, recurse_up=False, recurse_down=False): maker = Factory() # 2 Make a list of self + all children of same type tree = [self, [], 0, None] toproc = [tree] number_splits = 1 while toproc: curr = toproc.pop(0) for child in curr[0].get_children(): if isinstance(child, type(curr[0])): childset = [child, [], 0, curr] curr[1].append(childset) toproc.append(childset) number_splits += 1 else: curr[1].append([None, [], 1, None]) p = curr while p: p[2] += 1 p = p[3] # (1c) If Shift modifier, redistribute lower sections too if recurse_down and (maker.isinstance(child, 'VPaned') or maker.isinstance(child, 'HPaned')): child.do_redistribute(False, True) # 3 Get ancestor x/y => a, and handle size => hs avail_pixels = self.get_length() handle_size = self.get_handlesize() # 4 Math! eek (a - (n * hs)) / (n + 1) = single size => s single_size = (avail_pixels - (number_splits * handle_size)) / (number_splits + 1) arr_sizes = [single_size] * (number_splits + 1) for i in range(avail_pixels % (number_splits + 1)): arr_sizes[i] += 1 # 5 Descend down setting the handle position to s # (Has to handle nesting properly) toproc = [tree] while toproc: curr = toproc.pop(0) for child in curr[1]: toproc.append(child) if curr[1].index(child) == 0: curr[0].set_position((child[2] * single_size) + ((child[2] - 1) * handle_size))
def group_tab(self, widget): """Group all terminals in the current tab""" maker = Factory() notebook = self.get_child() if not maker.isinstance(notebook, 'Notebook'): dbg('not in a notebook, refusing to group tab') return pagenum = notebook.get_current_page() while True: group = _('Tab %d') % pagenum if group not in self.terminator.groups: break pagenum += 1 self.set_groups(group, self.get_visible_terminals())
def reconfigure(self): """Update configuration for the whole application""" if self.style_providers != []: for style_provider in self.style_providers: Gtk.StyleContext.remove_provider_for_screen( Gdk.Screen.get_default(), style_provider) self.style_providers = [] # Force the window background to be transparent for newer versions of # GTK3. We then have to fix all the widget backgrounds because the # widgets theming may not render it's own background. css = """ .terminator-terminal-window { background-color: alpha(@theme_bg_color,0); } .terminator-terminal-window .notebook.header, .terminator-terminal-window notebook header { background-color: @theme_bg_color; } .terminator-terminal-window .pane-separator { background-color: @theme_bg_color; } .terminator-terminal-window .terminator-terminal-searchbar { background-color: @theme_bg_color; } """ # Fix several themes that put a borders, corners, or backgrounds around # viewports, making the titlebar look bad. css += """ .terminator-terminal-window GtkViewport, .terminator-terminal-window viewport { border-width: 0px; border-radius: 0px; background-color: transparent; } """ # Add per profile snippets for setting the background of the HBox template = """ .terminator-profile-%s { background-color: alpha(%s, %s); } """ profiles = self.config.base.profiles for profile in profiles.keys(): if profiles[profile]['use_theme_colors']: # Create a dummy window/vte and realise it so it has correct # values to read from tmp_win = Gtk.Window() tmp_vte = Vte.Terminal() tmp_win.add(tmp_vte) tmp_win.realize() bgcolor = tmp_vte.get_style_context().get_background_color( Gtk.StateType.NORMAL) bgcolor = "#{0:02x}{1:02x}{2:02x}".format( int(bgcolor.red * 255), int(bgcolor.green * 255), int(bgcolor.blue * 255)) tmp_win.remove(tmp_vte) del tmp_vte del tmp_win else: bgcolor = Gdk.RGBA() bgcolor = profiles[profile]['background_color'] if profiles[profile]['background_type'] == 'transparent': bgalpha = profiles[profile]['background_darkness'] else: bgalpha = "1" munged_profile = "".join( [c if c.isalnum() else "-" for c in profile]) css += template % (munged_profile, bgcolor, bgalpha) style_provider = Gtk.CssProvider() style_provider.load_from_data(css.encode('utf8')) self.style_providers.append(style_provider) # Attempt to load some theme specific stylistic tweaks for appearances usr_theme_dir = os.path.expanduser('~/.local/share/themes') (head, _tail) = os.path.split(borg.__file__) app_theme_dir = os.path.join(head, 'themes') theme_name = self.gtk_settings.get_property('gtk-theme-name') theme_part_list = ['terminator.css'] if self.config[ 'extra_styling']: # checkbox_style - needs adding to prefs theme_part_list.append('terminator_styling.css') for theme_part_file in theme_part_list: for theme_dir in [usr_theme_dir, app_theme_dir]: path_to_theme_specific_css = os.path.join( theme_dir, theme_name, 'gtk-3.0/apps', theme_part_file) if os.path.isfile(path_to_theme_specific_css): style_provider = Gtk.CssProvider() style_provider.connect('parsing-error', self.on_css_parsing_error) try: style_provider.load_from_path( path_to_theme_specific_css) except GError: # Hmmm. Should we try to provide GTK version specific files here on failure? gtk_version_string = '.'.join([ str(Gtk.get_major_version()), str(Gtk.get_minor_version()), str(Gtk.get_micro_version()) ]) err('Error(s) loading css from %s into Gtk %s' % (path_to_theme_specific_css, gtk_version_string)) self.style_providers.append(style_provider) break # Size the GtkPaned splitter handle size. if self.config['handle_size'] in range(0, 21): css = '''.terminator-terminal-window GtkPaned, .terminator-terminal-window paned {{ min-width: {0}px; min-height: {0}px; }} '''.format(self.config['handle_size']) style_provider = Gtk.CssProvider() style_provider.load_from_data(css.encode()) self.style_providers.append(style_provider) # Apply the providers, incrementing priority so they don't cancel out # each other for idx in range(0, len(self.style_providers)): Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), self.style_providers[idx], Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + idx) # Cause all the terminals to reconfigure for terminal in self.terminals: terminal.reconfigure() # Reparse our keybindings self.keybindings.configure(self.config['keybindings']) # Update tab position if appropriate maker = Factory() for window in self.windows: child = window.get_child() if maker.isinstance(child, 'Notebook'): child.configure()
def layout_done(self): """Layout operations have finished, record that fact""" self.doing_layout = False maker = Factory() window_last_active_term_mapping = {} for window in self.windows: if window.is_child_notebook(): source = window.get_toplevel().get_children()[0] else: source = window window_last_active_term_mapping[window] = copy.copy( source.last_active_term) for terminal in self.terminals: if not terminal.pid: terminal.spawn_child() for window in self.windows: if window.is_child_notebook(): # For windows with a notebook notebook = window.get_toplevel().get_children()[0] # Cycle through pages by number for page in range(0, notebook.get_n_pages()): # Try and get the entry in the previously saved mapping mapping = window_last_active_term_mapping[window] page_last_active_term = mapping.get( notebook.get_nth_page(page), None) if page_last_active_term is None: # Couldn't find entry, so we find the first child of type Terminal children = notebook.get_nth_page(page).get_children() for page_last_active_term in children: if maker.isinstance(page_last_active_term, 'Terminal'): page_last_active_term = page_last_active_term.uuid break else: err('Should never reach here!') page_last_active_term = None if page_last_active_term is None: # Bail on this tab as we're having no luck here, continue with the next continue # Set the notebook entry, then ensure Terminal is visible and focussed urn = page_last_active_term.urn notebook.last_active_term[notebook.get_nth_page( page)] = page_last_active_term if urn: term = self.find_terminal_by_uuid(urn) if term: term.ensure_visible_and_focussed() else: # For windows without a notebook ensure Terminal is visible and focussed if window_last_active_term_mapping[window]: term = self.find_terminal_by_uuid( window_last_active_term_mapping[window].urn) term.ensure_visible_and_focussed() # Build list of new windows using prelayout list new_win_list = [] for window in self.windows: if window not in self.prelayout_windows: new_win_list.append(window) # Make sure all new windows get bumped to the top for window in new_win_list: window.show() window.grab_focus() try: t = GdkX11.x11_get_server_time(window.get_window()) except (TypeError, AttributeError): t = 0 window.get_window().focus(t) # Awful workaround to be sure that the last focused window is actually the one focused. # Don't ask, don't tell policy on this. Even this is not 100% if self.last_active_window: window = self.find_window_by_uuid(self.last_active_window.urn) count = 0 while count < 1000 and Gtk.events_pending(): count += 1 Gtk.main_iteration_do(False) window.show() window.grab_focus() try: t = GdkX11.x11_get_server_time(window.get_window()) except (TypeError, AttributeError): t = 0 window.get_window().focus(t) self.prelayout_windows = None
def is_child_notebook(self): """Returns True if this Window's child is a Notebook""" maker = Factory() return maker.isinstance(self.get_child(), 'Notebook')
def newtab(self, debugtab=False, widget=None, cwd=None, metadata=None, profile=None): """Add a new tab, optionally supplying a child widget""" dbg('making a new tab') maker = Factory() top_window = self.get_toplevel() if not widget: widget = maker.make('Terminal') if cwd: widget.set_cwd(cwd) if profile and self.config['always_split_with_profile']: widget.force_set_profile(None, profile) widget.spawn_child(debugserver=debugtab) elif profile and self.config['always_split_with_profile']: widget.force_set_profile(None, profile) signals = {'close-term': self.wrapcloseterm, 'split-horiz': self.split_horiz, 'split-vert': self.split_vert, 'title-change': self.propagate_title_change, 'unzoom': self.unzoom, 'tab-change': top_window.tab_change, 'group-all': top_window.group_all, 'group-all-toggle': top_window.group_all_toggle, 'ungroup-all': top_window.ungroup_all, 'group-tab': top_window.group_tab, 'group-tab-toggle': top_window.group_tab_toggle, 'ungroup-tab': top_window.ungroup_tab, 'move-tab': top_window.move_tab, 'tab-new': [top_window.tab_new, widget], 'navigate': top_window.navigate_terminal} if maker.isinstance(widget, 'Terminal'): for signal in signals: args = [] handler = signals[signal] if isinstance(handler, list): args = handler[1:] handler = handler[0] self.connect_child(widget, signal, handler, *args) if metadata and metadata.has_key('tabnum'): tabpos = metadata['tabnum'] else: tabpos = -1 label = TabLabel(self.window.get_title(), self) if metadata and metadata.has_key('label'): dbg('creating TabLabel with text: %s' % metadata['label']) label.set_custom_label(metadata['label']) label.connect('close-clicked', self.closetab) label.show_all() widget.show_all() dbg('inserting page at position: %s' % tabpos) self.insert_page(widget, None, tabpos) if maker.isinstance(widget, 'Terminal'): containers, objects = ([], [widget]) else: containers, objects = enumerate_descendants(widget) term_widget = None for term_widget in objects: if maker.isinstance(term_widget, 'Terminal'): self.set_last_active_term(term_widget.uuid) break self.set_tab_label(widget, label) self.child_set_property(widget, 'tab-expand', True) self.child_set_property(widget, 'tab-fill', True) self.set_tab_reorderable(widget, True) self.set_current_page(tabpos) self.show_all() if maker.isinstance(term_widget, 'Terminal'): widget.grab_focus()
class Paned(Container): """Base class for Paned Containers""" position = None maker = None ratio = 0.5 last_balance_time = 0 last_balance_args = None def __init__(self): """Class initialiser""" self.terminator = Terminator() self.maker = Factory() Container.__init__(self) self.signals.append({ 'name': 'resize-term', 'flags': GObject.SignalFlags.RUN_LAST, 'return_type': None, 'param_types': (GObject.TYPE_STRING, ) }) # pylint: disable-msg=W0613 def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst=True): """Default axis splitter. This should be implemented by subclasses""" order = None self.remove(widget) if vertical: container = VPaned() else: container = HPaned() self.get_toplevel().set_pos_by_ratio = True if not sibling: sibling = self.maker.make('terminal') sibling.set_cwd(cwd) if self.config['always_split_with_profile']: sibling.force_set_profile(None, widget.get_profile()) sibling.spawn_child() if widget.group and self.config['split_to_group']: sibling.set_group(None, widget.group) elif self.config['always_split_with_profile']: sibling.force_set_profile(None, widget.get_profile()) self.add(container) self.show_all() order = [widget, sibling] if widgetfirst is False: order.reverse() for terminal in order: container.add(terminal) self.show_all() sibling.grab_focus() while Gtk.events_pending(): Gtk.main_iteration_do(False) self.get_toplevel().set_pos_by_ratio = False def add(self, widget, metadata=None): """Add a widget to the container""" if len(self.children) == 0: self.pack1(widget, False, True) self.children.append(widget) elif len(self.children) == 1: if self.get_child1(): self.pack2(widget, False, True) else: self.pack1(widget, False, True) self.children.append(widget) else: raise ValueError('Paned widgets can only have two children') if self.maker.isinstance(widget, 'Terminal'): top_window = self.get_toplevel() signals = { 'close-term': self.wrapcloseterm, 'split-horiz': self.split_horiz, 'split-vert': self.split_vert, 'title-change': self.propagate_title_change, 'resize-term': self.resizeterm, 'size-allocate': self.new_size, 'zoom': top_window.zoom, 'tab-change': top_window.tab_change, 'group-all': top_window.group_all, 'group-all-toggle': top_window.group_all_toggle, 'ungroup-all': top_window.ungroup_all, 'group-tab': top_window.group_tab, 'group-tab-toggle': top_window.group_tab_toggle, 'ungroup-tab': top_window.ungroup_tab, 'move-tab': top_window.move_tab, 'maximise': [top_window.zoom, False], 'tab-new': [top_window.tab_new, widget], 'navigate': top_window.navigate_terminal, 'rotate-cw': [top_window.rotate, True], 'rotate-ccw': [top_window.rotate, False] } for signal in signals: args = [] handler = signals[signal] if isinstance(handler, list): args = handler[1:] handler = handler[0] self.connect_child(widget, signal, handler, *args) if metadata and \ 'had_focus' in metadata and \ metadata['had_focus'] == True: widget.grab_focus() elif isinstance(widget, Gtk.Paned): try: self.connect_child(widget, 'resize-term', self.resizeterm) self.connect_child(widget, 'size-allocate', self.new_size) except TypeError: err('Paned::add: %s has no signal resize-term' % widget) def on_button_press(self, widget, event): """Handle button presses on a Pane""" if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS: if event.get_state( ) & Gdk.ModifierType.MOD4_MASK == Gdk.ModifierType.MOD4_MASK: recurse_up = True else: recurse_up = False if event.get_state( ) & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK: recurse_down = True else: recurse_down = False self.last_balance_time = time.time() self.last_balance_args = (recurse_up, recurse_down) return True else: return False def on_button_release(self, widget, event): """Handle button presses on a Pane""" if event.button == 1: if self.last_balance_time > (time.time() - 1): # Dumb loop still needed, or some terms get squished on a Super rebalance for i in range(3): while Gtk.events_pending(): Gtk.main_iteration_do(False) self.do_redistribute(*self.last_balance_args) return False def set_autoresize(self, autoresize): """Must be called on the highest ancestor in one given orientation""" """TODO write some better doc :)""" maker = Factory() children = self.get_children() self.child_set_property(children[0], 'resize', False) self.child_set_property(children[1], 'resize', not autoresize) for child in children: if maker.type(child) == maker.type(self): child.set_autoresize(autoresize) def do_redistribute(self, recurse_up=False, recurse_down=False): """Evenly divide available space between sibling panes""" maker = Factory() #1 Find highest ancestor of the same type => ha highest_ancestor = self while type(highest_ancestor.get_parent()) == type(highest_ancestor): highest_ancestor = highest_ancestor.get_parent() highest_ancestor.set_autoresize(False) # (1b) If Super modifier, redistribute higher sections too if recurse_up: grandfather = highest_ancestor.get_parent() if maker.isinstance(grandfather, 'VPaned') or \ maker.isinstance(grandfather, 'HPaned') : grandfather.do_redistribute(recurse_up, recurse_down) highest_ancestor._do_redistribute(recurse_up, recurse_down) GLib.idle_add(highest_ancestor.set_autoresize, True) def _do_redistribute(self, recurse_up=False, recurse_down=False): maker = Factory() #2 Make a list of self + all children of same type tree = [self, [], 0, None] toproc = [tree] number_splits = 1 while toproc: curr = toproc.pop(0) for child in curr[0].get_children(): if type(child) == type(curr[0]): childset = [child, [], 0, curr] curr[1].append(childset) toproc.append(childset) number_splits = number_splits + 1 else: curr[1].append([None, [], 1, None]) p = curr while p: p[2] = p[2] + 1 p = p[3] # (1c) If Shift modifier, redistribute lower sections too if recurse_down and \ (maker.isinstance(child, 'VPaned') or \ maker.isinstance(child, 'HPaned')): child.do_redistribute(False, True) #3 Get ancestor x/y => a, and handle size => hs avail_pixels = self.get_length() handle_size = self.get_handlesize() #4 Math! eek (a - (n * hs)) / (n + 1) = single size => s single_size = (avail_pixels - (number_splits * handle_size)) / (number_splits + 1) arr_sizes = [single_size] * (number_splits + 1) for i in range(avail_pixels % (number_splits + 1)): arr_sizes[i] = arr_sizes[i] + 1 #5 Descend down setting the handle position to s # (Has to handle nesting properly) toproc = [tree] while toproc: curr = toproc.pop(0) for child in curr[1]: toproc.append(child) if curr[1].index(child) == 0: curr[0].set_position((child[2] * single_size) + ((child[2] - 1) * handle_size)) def remove(self, widget): """Remove a widget from the container""" Gtk.Paned.remove(self, widget) self.disconnect_child(widget) self.children.remove(widget) return True def get_children(self): """Return an ordered list of our children""" children = [] children.append(self.get_child1()) children.append(self.get_child2()) return children def get_child_metadata(self, widget): """Return metadata about a child""" metadata = {} metadata['had_focus'] = widget.has_focus() def get_handlesize(self): """Why oh why, gtk3?""" try: value = GObject.Value(int) self.style_get_property('handle-size', value) return value.get_int() except: return 0 def wrapcloseterm(self, widget): """A child terminal has closed, so this container must die""" dbg('Paned::wrapcloseterm: Called on %s' % widget) if self.closeterm(widget): # At this point we only have one child, which is the surviving term sibling = self.children[0] first_term_sibling = sibling cur_tabnum = None focus_sibling = True if self.get_toplevel().is_child_notebook(): notebook = self.get_toplevel().get_children()[0] cur_tabnum = notebook.get_current_page() tabnum = notebook.page_num_descendant(self) nth_page = notebook.get_nth_page(tabnum) exiting_term_was_last_active = ( notebook.last_active_term[nth_page] == widget.uuid) if exiting_term_was_last_active: first_term_sibling = enumerate_descendants(self)[1][0] notebook.set_last_active_term(first_term_sibling.uuid) notebook.clean_last_active_term() self.get_toplevel().last_active_term = None if cur_tabnum != tabnum: focus_sibling = False elif self.get_toplevel().last_active_term != widget.uuid: focus_sibling = False self.remove(sibling) metadata = None parent = self.get_parent() metadata = parent.get_child_metadata(self) dbg('metadata obtained for %s: %s' % (self, metadata)) parent.remove(self) self.cnxids.remove_all() parent.add(sibling, metadata) if cur_tabnum: notebook.set_current_page(cur_tabnum) if focus_sibling: first_term_sibling.grab_focus() elif not sibling.get_toplevel().is_child_notebook(): Terminator().find_terminal_by_uuid( sibling.get_toplevel().last_active_term.urn).grab_focus() else: dbg("Paned::wrapcloseterm: self.closeterm failed") def hoover(self): """Check that we still have a reason to exist""" if len(self.children) == 1: dbg('Paned::hoover: We only have one child, die') parent = self.get_parent() child = self.children[0] self.remove(child) parent.replace(self, child) del self def resizeterm(self, widget, keyname): """Handle a keyboard event requesting a terminal resize""" if keyname in ['up', 'down'] and isinstance(self, Gtk.VPaned): # This is a key we can handle position = self.get_position() if self.maker.isinstance(widget, 'Terminal'): fontheight = widget.vte.get_char_height() else: fontheight = 10 if keyname == 'up': self.set_position(position - fontheight) else: self.set_position(position + fontheight) elif keyname in ['left', 'right'] and isinstance(self, Gtk.HPaned): # This is a key we can handle position = self.get_position() if self.maker.isinstance(widget, 'Terminal'): fontwidth = widget.vte.get_char_width() else: fontwidth = 10 if keyname == 'left': self.set_position(position - fontwidth) else: self.set_position(position + fontwidth) else: # This is not a key we can handle self.emit('resize-term', keyname) def create_layout(self, layout): """Apply layout configuration""" if 'children' not in layout: err('layout specifies no children: %s' % layout) return children = layout['children'] if len(children) != 2: # Paned widgets can only have two children err('incorrect number of children for Paned: %s' % layout) return keys = [] # FIXME: This seems kinda ugly. All we want here is to know the order # of children based on child['order'] try: child_order_map = {} for child in children: key = children[child]['order'] child_order_map[key] = child map_keys = child_order_map.keys() map_keys.sort() for map_key in map_keys: keys.append(child_order_map[map_key]) except KeyError: # We've failed to figure out the order. At least give the terminals # in the wrong order keys = children.keys() num = 0 for child_key in keys: child = children[child_key] dbg('Making a child of type: %s' % child['type']) if child['type'] == 'Terminal': pass elif child['type'] == 'VPaned': if num == 0: terminal = self.get_child1() else: terminal = self.get_child2() self.split_axis(terminal, True) elif child['type'] == 'HPaned': if num == 0: terminal = self.get_child1() else: terminal = self.get_child2() self.split_axis(terminal, False) else: err('unknown child type: %s' % child['type']) num = num + 1 self.get_child1().create_layout(children[keys[0]]) self.get_child2().create_layout(children[keys[1]]) # Set the position with ratio. For some reason more reliable than by pos. if 'ratio' in layout: self.ratio = float(layout['ratio']) self.set_position_by_ratio() def grab_focus(self): """We don't want focus, we want a Terminal to have it""" self.get_child1().grab_focus() def rotate_recursive(self, parent, w, h, clockwise): """ Recursively rotate "self" into a new paned that'll have "w" x "h" size. Attach it to "parent". As discussed in LP#1522542, we should build up the new layout (including the separator positions) in a single step. We can't rely on Gtk+ computing the allocation sizes yet, so we have to do the computation ourselves and carry the resulting paned sizes all the way down the widget tree. """ maker = Factory() handle_size = self.get_handlesize() if isinstance(self, HPaned): container = VPaned() reverse = not clockwise else: container = HPaned() reverse = clockwise container.ratio = self.ratio children = self.get_children() if reverse: container.ratio = 1 - container.ratio children.reverse() if isinstance(self, HPaned): w1 = w2 = w h1 = pos = self.position_by_ratio(h, handle_size, container.ratio) h2 = max(h - h1 - handle_size, 0) else: h1 = h2 = h w1 = pos = self.position_by_ratio(w, handle_size, container.ratio) w2 = max(w - w1 - handle_size, 0) container.set_pos(pos) parent.add(container) if maker.isinstance(children[0], 'Terminal'): children[0].get_parent().remove(children[0]) container.add(children[0]) else: children[0].rotate_recursive(container, w1, h1, clockwise) if maker.isinstance(children[1], 'Terminal'): children[1].get_parent().remove(children[1]) container.add(children[1]) else: children[1].rotate_recursive(container, w2, h2, clockwise) def new_size(self, widget, allocation): if self.get_toplevel().set_pos_by_ratio: self.set_position_by_ratio() else: self.set_position(self.get_position()) def position_by_ratio(self, total_size, handle_size, ratio): non_separator_size = max(total_size - handle_size, 0) ratio = min(max(ratio, 0.0), 1.0) return int(round(non_separator_size * ratio)) def ratio_by_position(self, total_size, handle_size, position): non_separator_size = max(total_size - handle_size, 0) if non_separator_size == 0: return None position = min(max(position, 0), non_separator_size) return float(position) / float(non_separator_size) def set_position_by_ratio(self): # Fix for strange race condition where every so often get_length returns 1. (LP:1655027) while self.terminator.doing_layout and self.get_length() == 1: while Gtk.events_pending(): Gtk.main_iteration() self.set_pos( self.position_by_ratio(self.get_length(), self.get_handlesize(), self.ratio)) def set_position(self, pos): newratio = self.ratio_by_position(self.get_length(), self.get_handlesize(), pos) if newratio is not None: self.ratio = newratio self.set_pos(pos)