2686296cf6
Note: wmclass_name no longer readable under GTK3, so -c option is a bit dubious now. Also, in HighContrast, there are issues with titlebar background colours that are not present in GTK2.
925 lines
32 KiB
Python
Executable File
925 lines
32 KiB
Python
Executable File
#!/usr/bin/python
|
|
# Terminator by Chris Jones <cmsj@tenshu.net>
|
|
# GPL v2 only
|
|
"""window.py - class for the main Terminator window"""
|
|
|
|
import copy
|
|
import time
|
|
import uuid
|
|
import gi
|
|
from gi.repository import GObject
|
|
from gi.repository import Gtk, Gdk
|
|
from gi.repository import Keybinder
|
|
|
|
from util import dbg, err, make_uuid
|
|
import util
|
|
from translation import _
|
|
from version import APP_NAME
|
|
from container import Container
|
|
from factory import Factory
|
|
from terminator import Terminator
|
|
|
|
try:
|
|
from gi.repository import Keybinder
|
|
except ImportError:
|
|
err('Warning: python-keybinder is not installed. This means the \
|
|
hide_window shortcut will be unavailable')
|
|
|
|
# pylint: disable-msg=R0904
|
|
class Window(Container, Gtk.Window):
|
|
"""Class implementing a top-level Terminator window"""
|
|
|
|
terminator = None
|
|
title = None
|
|
isfullscreen = None
|
|
ismaximised = None
|
|
hidebound = None
|
|
hidefunc = None
|
|
losefocus_time = 0
|
|
position = None
|
|
ignore_startup_show = None
|
|
set_pos_by_ratio = None
|
|
last_active_term = None
|
|
|
|
zoom_data = None
|
|
|
|
term_zoomed = False
|
|
__gproperties__ = {
|
|
'term_zoomed': (GObject.TYPE_BOOLEAN,
|
|
'terminal zoomed',
|
|
'whether the terminal is zoomed',
|
|
False,
|
|
GObject.PARAM_READWRITE)
|
|
}
|
|
|
|
def __init__(self):
|
|
"""Class initialiser"""
|
|
self.terminator = Terminator()
|
|
self.terminator.register_window(self)
|
|
|
|
Container.__init__(self)
|
|
GObject.GObject.__init__(self)
|
|
GObject.type_register(Window)
|
|
self.register_signals(Window)
|
|
|
|
# self.set_property('allow-shrink', True) # FIXME FOR GTK3, or do we need this actually?
|
|
icon_to_apply=''
|
|
|
|
self.register_callbacks()
|
|
self.apply_config()
|
|
|
|
self.title = WindowTitle(self)
|
|
self.title.update()
|
|
|
|
options = self.config.options_get()
|
|
if options:
|
|
if options.forcedtitle:
|
|
self.title.force_title(options.forcedtitle)
|
|
|
|
if options.role:
|
|
self.set_role(options.role)
|
|
|
|
if options.classname is not None:
|
|
self.set_wmclass(options.classname, self.wmclass_class)
|
|
|
|
if options.forcedicon is not None:
|
|
icon_to_apply = options.forcedicon
|
|
|
|
if options.geometry:
|
|
if not self.parse_geometry(options.geometry):
|
|
err('Window::__init__: Unable to parse geometry: %s' %
|
|
options.geometry)
|
|
|
|
self.apply_icon(icon_to_apply)
|
|
self.pending_set_rough_geometry_hint = False
|
|
|
|
def do_get_property(self, prop):
|
|
"""Handle gobject getting a property"""
|
|
if prop.name in ['term_zoomed', 'term-zoomed']:
|
|
return(self.term_zoomed)
|
|
else:
|
|
raise AttributeError('unknown property %s' % prop.name)
|
|
|
|
def do_set_property(self, prop, value):
|
|
"""Handle gobject setting a property"""
|
|
if prop.name in ['term_zoomed', 'term-zoomed']:
|
|
self.term_zoomed = value
|
|
else:
|
|
raise AttributeError('unknown property %s' % prop.name)
|
|
|
|
def register_callbacks(self):
|
|
"""Connect the GTK+ signals we care about"""
|
|
self.connect('key-press-event', self.on_key_press)
|
|
self.connect('button-press-event', self.on_button_press)
|
|
self.connect('delete_event', self.on_delete_event)
|
|
self.connect('destroy', self.on_destroy_event)
|
|
self.connect('window-state-event', self.on_window_state_changed)
|
|
self.connect('focus-out-event', self.on_focus_out)
|
|
self.connect('focus-in-event', self.on_focus_in)
|
|
|
|
# Attempt to grab a global hotkey for hiding the window.
|
|
# If we fail, we'll never hide the window, iconifying instead.
|
|
if self.config['keybindings']['hide_window'] != None:
|
|
try:
|
|
self.hidebound = Keybinder.bind(
|
|
self.config['keybindings']['hide_window'],
|
|
self.on_hide_window)
|
|
except (KeyError, NameError):
|
|
pass
|
|
|
|
if not self.hidebound:
|
|
err('Unable to bind hide_window key, another instance/window has it.')
|
|
self.hidefunc = self.iconify
|
|
else:
|
|
self.hidefunc = self.hide
|
|
|
|
def apply_config(self):
|
|
"""Apply various configuration options"""
|
|
options = self.config.options_get()
|
|
maximise = self.config['window_state'] == 'maximise'
|
|
fullscreen = self.config['window_state'] == 'fullscreen'
|
|
hidden = self.config['window_state'] == 'hidden'
|
|
borderless = self.config['borderless']
|
|
skiptaskbar = self.config['hide_from_taskbar']
|
|
alwaysontop = self.config['always_on_top']
|
|
sticky = self.config['sticky']
|
|
|
|
if options:
|
|
if options.maximise:
|
|
maximise = True
|
|
if options.fullscreen:
|
|
fullscreen = True
|
|
if options.hidden:
|
|
hidden = True
|
|
if options.borderless:
|
|
borderless = True
|
|
|
|
self.set_fullscreen(fullscreen)
|
|
self.set_maximised(maximise)
|
|
self.set_borderless(borderless)
|
|
self.set_always_on_top(alwaysontop)
|
|
self.set_real_transparency()
|
|
self.set_sticky(sticky)
|
|
if self.hidebound:
|
|
self.set_hidden(hidden)
|
|
self.set_skip_taskbar_hint(skiptaskbar)
|
|
else:
|
|
self.set_iconified(hidden)
|
|
|
|
def apply_icon(self, requested_icon):
|
|
"""Set the window icon"""
|
|
icon_theme = Gtk.IconTheme.get_default()
|
|
icon_name_list = [APP_NAME] # disable self.wmclass_name, n/a in GTK3
|
|
|
|
if requested_icon:
|
|
try:
|
|
self.set_icon_from_file(requested_icon)
|
|
return
|
|
except (NameError, GObject.GError):
|
|
dbg('Unable to load %s icon as file' % (repr(requested_icon)))
|
|
|
|
icon_name_list.insert(0, requested_icon)
|
|
|
|
for icon_name in icon_name_list:
|
|
# Test if the icon is available first
|
|
if icon_theme.lookup_icon(icon_name, 48, 0):
|
|
self.set_icon_name(icon_name)
|
|
return # Success! We're done.
|
|
else:
|
|
dbg('Unable to load %s icon' % (icon_name))
|
|
|
|
icon = self.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.ICON_SIZE_BUTTON)
|
|
self.set_icon(icon)
|
|
|
|
def on_key_press(self, window, event):
|
|
"""Handle a keyboard event"""
|
|
maker = Factory()
|
|
|
|
self.set_urgency_hint(False)
|
|
|
|
mapping = self.terminator.keybindings.lookup(event)
|
|
|
|
if mapping:
|
|
dbg('Window::on_key_press: looked up %r' % mapping)
|
|
if mapping == 'full_screen':
|
|
self.set_fullscreen(not self.isfullscreen)
|
|
elif mapping == 'close_window':
|
|
if not self.on_delete_event(window,
|
|
Gdk.Event(Gdk.DELETE)):
|
|
self.on_destroy_event(window,
|
|
Gdk.Event(Gdk.DESTROY))
|
|
elif mapping == 'new_tab':
|
|
self.tab_new(self.get_focussed_terminal())
|
|
else:
|
|
return(False)
|
|
return(True)
|
|
|
|
def on_button_press(self, window, event):
|
|
"""Handle a mouse button event. Mainly this is just a clean way to
|
|
cancel any urgency hints that are set."""
|
|
self.set_urgency_hint(False)
|
|
return(False)
|
|
|
|
def on_focus_out(self, window, event):
|
|
"""Focus has left the window"""
|
|
for terminal in self.get_visible_terminals():
|
|
terminal.on_window_focus_out()
|
|
|
|
self.losefocus_time = time.time()
|
|
if self.config['hide_on_lose_focus'] and self.get_property('visible'):
|
|
self.position = self.get_position()
|
|
self.hidefunc()
|
|
|
|
def on_focus_in(self, window, event):
|
|
"""Focus has entered the window"""
|
|
self.set_urgency_hint(False)
|
|
if not self.terminator.doing_layout:
|
|
self.terminator.last_active_window = self.uuid
|
|
# FIXME: Cause the terminal titlebars to update here
|
|
|
|
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 tab_new(self, widget=None, debugtab=False, _param1=None, _param2=None):
|
|
"""Make a new tab"""
|
|
cwd = None
|
|
profile = None
|
|
|
|
if self.get_property('term_zoomed') == True:
|
|
err("You can't create a tab while a terminal is maximised/zoomed")
|
|
return
|
|
|
|
if widget:
|
|
cwd = widget.get_cwd()
|
|
profile = widget.get_profile()
|
|
|
|
maker = Factory()
|
|
if not self.is_child_notebook():
|
|
dbg('Making a new Notebook')
|
|
notebook = maker.make('Notebook', window=self)
|
|
self.show()
|
|
self.present()
|
|
return self.get_child().newtab(debugtab, cwd=cwd, profile=profile)
|
|
|
|
def on_delete_event(self, window, event, data=None):
|
|
"""Handle a window close request"""
|
|
maker = Factory()
|
|
if maker.isinstance(self.get_child(), 'Terminal'):
|
|
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 confirm_close(self, window, type):
|
|
"""Display a confirmation dialog when the user is closing multiple
|
|
terminals in one window"""
|
|
|
|
return(not (self.construct_confirm_close(window, type) == Gtk.ResponseType.ACCEPT))
|
|
|
|
def on_destroy_event(self, widget, data=None):
|
|
"""Handle window destruction"""
|
|
dbg('destroying self')
|
|
for terminal in self.get_visible_terminals():
|
|
terminal.close()
|
|
self.cnxids.remove_all()
|
|
self.terminator.deregister_window(self)
|
|
self.destroy()
|
|
del(self)
|
|
|
|
def on_hide_window(self, data=None):
|
|
"""Handle a request to hide/show the window"""
|
|
|
|
if not self.get_property('visible'):
|
|
#Don't show if window has just been hidden because of
|
|
#lost focus
|
|
if (time.time() - self.losefocus_time < 0.1) and \
|
|
self.config['hide_on_lose_focus']:
|
|
return
|
|
if self.position:
|
|
self.move(self.position[0], self.position[1])
|
|
self.show()
|
|
self.grab_focus()
|
|
try:
|
|
t = GdkX11.x11_get_server_time(self.window)
|
|
except AttributeError:
|
|
t = 0
|
|
self.window.focus(t)
|
|
else:
|
|
self.position = self.get_position()
|
|
self.hidefunc()
|
|
|
|
# pylint: disable-msg=W0613
|
|
def on_window_state_changed(self, window, event):
|
|
"""Handle the state of the window changing"""
|
|
self.isfullscreen = bool(event.new_window_state &
|
|
Gdk.WindowState.FULLSCREEN)
|
|
self.ismaximised = bool(event.new_window_state &
|
|
Gdk.WindowState.MAXIMIZED)
|
|
dbg('Window::on_window_state_changed: fullscreen=%s, maximised=%s' \
|
|
% (self.isfullscreen, self.ismaximised))
|
|
|
|
return(False)
|
|
|
|
def set_maximised(self, value):
|
|
"""Set the maximised state of the window from the supplied value"""
|
|
if value == True:
|
|
self.maximize()
|
|
else:
|
|
self.unmaximize()
|
|
|
|
def set_fullscreen(self, value):
|
|
"""Set the fullscreen state of the window from the supplied value"""
|
|
if value == True:
|
|
self.fullscreen()
|
|
else:
|
|
self.unfullscreen()
|
|
|
|
def set_borderless(self, value):
|
|
"""Set the state of the window border from the supplied value"""
|
|
self.set_decorated (not value)
|
|
|
|
def set_hidden(self, value):
|
|
"""Set the visibility of the window from the supplied value"""
|
|
if value == True:
|
|
self.ignore_startup_show = True
|
|
else:
|
|
self.ignore_startup_show = False
|
|
|
|
def set_iconified(self, value):
|
|
"""Set the minimised state of the window from the supplied value"""
|
|
if value == True:
|
|
self.iconify()
|
|
|
|
def set_always_on_top(self, value):
|
|
"""Set the always on top window hint from the supplied value"""
|
|
self.set_keep_above(value)
|
|
|
|
def set_sticky(self, value):
|
|
"""Set the sticky hint from the supplied value"""
|
|
if value == True:
|
|
self.stick()
|
|
|
|
def set_real_transparency(self, value=True):
|
|
"""Enable RGBA if supported on the current screen"""
|
|
if self.is_composited() == False:
|
|
value = False
|
|
|
|
screen = self.get_screen()
|
|
if value:
|
|
dbg('setting rgba visual')
|
|
visual = screen.get_rgba_visual()
|
|
if visual:
|
|
self.set_visual(visual)
|
|
|
|
def show(self, startup=False):
|
|
"""Undo the startup show request if started in hidden mode"""
|
|
#Present is necessary to grab focus when window is hidden from taskbar.
|
|
#It is important to call present() before show(), otherwise the window
|
|
#won't be brought to front if an another application has the focus.
|
|
#Last note: present() will implicitly call Gtk.Window.show()
|
|
self.present()
|
|
|
|
#Window must be shown, then hidden for the hotkeys to be registered
|
|
if (self.ignore_startup_show and startup == True):
|
|
self.position = self.get_position()
|
|
self.hide()
|
|
|
|
|
|
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 remove(self, widget):
|
|
"""Remove our child widget by way of Gtk.Window.remove()"""
|
|
Gtk.Window.remove(self, widget)
|
|
self.disconnect_child(widget)
|
|
return(True)
|
|
|
|
def get_children(self):
|
|
"""Return a single list of our child"""
|
|
children = []
|
|
children.append(self.get_child())
|
|
return(children)
|
|
|
|
def hoover(self):
|
|
"""Ensure we still have a reason to exist"""
|
|
if not self.get_child():
|
|
self.emit('destroy')
|
|
|
|
def closeterm(self, widget):
|
|
"""Handle a terminal closing"""
|
|
Container.closeterm(self, widget)
|
|
self.hoover()
|
|
|
|
def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst=True):
|
|
"""Split the window"""
|
|
if self.get_property('term_zoomed') == True:
|
|
err("You can't split while a terminal is maximised/zoomed")
|
|
return
|
|
|
|
order = None
|
|
maker = Factory()
|
|
self.remove(widget)
|
|
|
|
if vertical:
|
|
container = maker.make('VPaned')
|
|
else:
|
|
container = maker.make('HPaned')
|
|
|
|
self.set_pos_by_ratio = True
|
|
|
|
if not sibling:
|
|
sibling = maker.make('Terminal')
|
|
sibling.set_cwd(cwd)
|
|
sibling.spawn_child()
|
|
if widget.group and self.config['split_to_group']:
|
|
sibling.set_group(None, widget.group)
|
|
if self.config['always_split_with_profile']:
|
|
sibling.force_set_profile(None, widget.get_profile())
|
|
|
|
self.add(container)
|
|
container.show_all()
|
|
|
|
order = [widget, sibling]
|
|
if widgetfirst is False:
|
|
order.reverse()
|
|
|
|
for term in order:
|
|
container.add(term)
|
|
container.show_all()
|
|
sibling.grab_focus()
|
|
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
self.set_pos_by_ratio = False
|
|
|
|
|
|
def zoom(self, widget, font_scale=True):
|
|
"""Zoom a terminal widget"""
|
|
children = self.get_children()
|
|
|
|
if widget in children:
|
|
# This widget is a direct child of ours and we're a Window
|
|
# so zooming is a no-op
|
|
return
|
|
|
|
self.zoom_data = widget.get_zoom_data()
|
|
self.zoom_data['widget'] = widget
|
|
self.zoom_data['old_child'] = children[0]
|
|
self.zoom_data['font_scale'] = font_scale
|
|
|
|
self.remove(self.zoom_data['old_child'])
|
|
self.zoom_data['old_parent'].remove(widget)
|
|
self.add(widget)
|
|
self.set_property('term_zoomed', True)
|
|
|
|
if font_scale:
|
|
widget.cnxids.new(widget, 'size-allocate',
|
|
widget.zoom_scale, self.zoom_data)
|
|
|
|
widget.grab_focus()
|
|
|
|
def unzoom(self, widget):
|
|
"""Restore normal terminal layout"""
|
|
if not self.get_property('term_zoomed'):
|
|
# We're not zoomed anyway
|
|
dbg('Window::unzoom: not zoomed, no-op')
|
|
return
|
|
|
|
widget = self.zoom_data['widget']
|
|
if self.zoom_data['font_scale']:
|
|
widget.vte.set_font(self.zoom_data['old_font'])
|
|
|
|
self.remove(widget)
|
|
self.add(self.zoom_data['old_child'])
|
|
self.zoom_data['old_parent'].add(widget)
|
|
widget.grab_focus()
|
|
self.zoom_data = None
|
|
self.set_property('term_zoomed', False)
|
|
|
|
def rotate(self, widget, clockwise):
|
|
"""Rotate children in this window"""
|
|
self.set_pos_by_ratio = True
|
|
maker = Factory()
|
|
# collect all paned children in breadth-first order
|
|
paned = []
|
|
for child in self.get_children():
|
|
if maker.isinstance(child, 'Paned'):
|
|
paned.append(child)
|
|
for p in paned:
|
|
for child in p.get_children():
|
|
if child not in paned and maker.isinstance(child, 'Paned'):
|
|
paned.append(child)
|
|
# then propagate the rotation
|
|
for p in paned:
|
|
p.rotate(widget, clockwise)
|
|
self.show_all()
|
|
widget.grab_focus()
|
|
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
self.set_pos_by_ratio = False
|
|
|
|
def get_visible_terminals(self):
|
|
"""Walk down the widget tree to find all of the visible terminals.
|
|
Mostly using Container::get_visible_terminals()"""
|
|
terminals = {}
|
|
if not hasattr(self, 'cached_maker'):
|
|
self.cached_maker = Factory()
|
|
maker = self.cached_maker
|
|
child = self.get_child()
|
|
|
|
if not child:
|
|
return([])
|
|
|
|
# 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, 'Container'):
|
|
terminals.update(child.get_visible_terminals())
|
|
elif maker.isinstance(child, 'Terminal'):
|
|
terminals[child] = child.get_allocation()
|
|
else:
|
|
err('Unknown child type %s' % type(child))
|
|
|
|
return(terminals)
|
|
|
|
def get_focussed_terminal(self):
|
|
"""Find which terminal we want to have focus"""
|
|
terminals = self.get_visible_terminals()
|
|
for terminal in terminals:
|
|
if terminal.vte.is_focus():
|
|
return(terminal)
|
|
return(None)
|
|
|
|
def deferred_set_rough_geometry_hints(self):
|
|
# no parameters are used in set_rough_geometry_hints, so we can
|
|
# use the set_rough_geometry_hints
|
|
if self.pending_set_rough_geometry_hint == True:
|
|
return
|
|
self.pending_set_rough_geometry_hint = True
|
|
GObject.idle_add(self.do_deferred_set_rough_geometry_hints)
|
|
|
|
def do_deferred_set_rough_geometry_hints(self):
|
|
self.pending_set_rough_geometry_hint = False
|
|
self.set_rough_geometry_hints()
|
|
|
|
def set_rough_geometry_hints(self):
|
|
"""Walk all the terminals along the top and left edges to fake up how
|
|
many columns/rows we sort of have"""
|
|
if self.ismaximised == True:
|
|
return
|
|
if not hasattr(self, 'cached_maker'):
|
|
self.cached_maker = Factory()
|
|
maker = self.cached_maker
|
|
if maker.isinstance(self.get_child(), 'Notebook'):
|
|
dbg("We don't currently support geometry hinting with tabs")
|
|
return
|
|
|
|
terminals = self.get_visible_terminals()
|
|
column_sum = 0
|
|
row_sum = 0
|
|
|
|
for terminal in terminals:
|
|
rect = terminal.get_allocation()
|
|
if rect.x == 0:
|
|
cols, rows = terminal.get_size()
|
|
row_sum = row_sum + rows
|
|
if rect.y == 0:
|
|
cols, rows = terminal.get_size()
|
|
column_sum = column_sum + cols
|
|
|
|
if column_sum == 0 or row_sum == 0:
|
|
dbg('column_sum=%s,row_sum=%s. No terminals found in >=1 axis' %
|
|
(column_sum, row_sum))
|
|
return
|
|
|
|
# FIXME: I don't think we should just use whatever font size info is on
|
|
# the last terminal we inspected. Looking up the default profile font
|
|
# size and calculating its character sizes would be rather expensive
|
|
# though.
|
|
font_width, font_height = terminal.get_font_size()
|
|
total_font_width = font_width * column_sum
|
|
total_font_height = font_height * row_sum
|
|
|
|
win_width, win_height = self.get_size()
|
|
extra_width = win_width - total_font_width
|
|
extra_height = win_height - total_font_height
|
|
|
|
dbg('setting geometry hints: (ewidth:%s)(eheight:%s),\
|
|
(fwidth:%s)(fheight:%s)' % (extra_width, extra_height,
|
|
font_width, font_height))
|
|
geometry = Gdk.Geometry()
|
|
geometry.base_width = extra_width
|
|
geometry.base_height = extra_height
|
|
geometry.width_inc = font_width
|
|
geometry.height_inc = font_height
|
|
self.set_geometry_hints(self, geometry, Gdk.WindowHints.BASE_SIZE | Gdk.WindowHints.RESIZE_INC)
|
|
|
|
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 set_groups(self, new_group, term_list):
|
|
"""Set terminals in term_list to new_group"""
|
|
for terminal in term_list:
|
|
terminal.set_group(None, new_group)
|
|
self.terminator.focus_changed(self.terminator.last_focused_term)
|
|
|
|
def group_all(self, widget):
|
|
"""Group all terminals"""
|
|
# FIXME: Why isn't this being done by Terminator() ?
|
|
group = _('All')
|
|
self.terminator.create_group(group)
|
|
self.set_groups(group, self.terminator.terminals)
|
|
|
|
def group_all_toggle(self, widget):
|
|
"""Toggle grouping to all"""
|
|
if widget.group == 'All':
|
|
self.ungroup_all(widget)
|
|
else:
|
|
self.group_all(widget)
|
|
|
|
def ungroup_all(self, widget):
|
|
"""Ungroup all terminals"""
|
|
self.set_groups(None, self.terminator.terminals)
|
|
|
|
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 group_tab_toggle(self, widget):
|
|
"""Blah"""
|
|
if widget.group and widget.group[:4] == 'Tab ':
|
|
self.ungroup_tab(widget)
|
|
else:
|
|
self.group_tab(widget)
|
|
|
|
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 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 navigate_terminal(self, terminal, direction):
|
|
"""Navigate around terminals"""
|
|
_containers, terminals = util.enumerate_descendants(self)
|
|
visibles = self.get_visible_terminals()
|
|
current = terminals.index(terminal)
|
|
length = len(terminals)
|
|
next = None
|
|
|
|
if length <= 1 or len(visibles) <= 1:
|
|
return
|
|
|
|
if direction in ['next', 'prev']:
|
|
tmpterms = copy.copy(terminals)
|
|
tmpterms = tmpterms[current+1:]
|
|
tmpterms.extend(terminals[0:current])
|
|
|
|
if direction == 'next':
|
|
tmpterms.reverse()
|
|
|
|
next = 0
|
|
while len(tmpterms) > 0:
|
|
tmpitem = tmpterms.pop()
|
|
if tmpitem in visibles:
|
|
next = terminals.index(tmpitem)
|
|
break
|
|
elif direction in ['left', 'right', 'up', 'down']:
|
|
layout = self.get_visible_terminals()
|
|
allocation = terminal.get_allocation()
|
|
possibles = []
|
|
|
|
# Get the co-ordinate of the appropriate edge for this direction
|
|
edge, p1, p2 = util.get_edge(allocation, direction)
|
|
# Find all visible terminals which are, in their entirity, in the
|
|
# direction we want to move, and are at least partially spanning
|
|
# p1 to p2
|
|
for term in layout:
|
|
rect = layout[term]
|
|
if util.get_nav_possible(edge, rect, direction, p1, p2):
|
|
possibles.append(term)
|
|
|
|
if len(possibles) == 0:
|
|
return
|
|
|
|
# Find out how far away each of the possible terminals is, then
|
|
# find the smallest distance. The winning terminals are all of
|
|
# those who are that distance away.
|
|
offsets = {}
|
|
for term in possibles:
|
|
rect = layout[term]
|
|
offsets[term] = util.get_nav_offset(edge, rect, direction)
|
|
keys = offsets.values()
|
|
keys.sort()
|
|
winners = [k for k, v in offsets.iteritems() if v == keys[0]]
|
|
next = terminals.index(winners[0])
|
|
|
|
if len(winners) > 1:
|
|
# Break an n-way tie using the cursor position
|
|
term_alloc = terminal.get_allocation()
|
|
cursor_x = term_alloc.x + term_alloc.width / 2
|
|
cursor_y = term_alloc.y + term_alloc.height / 2
|
|
|
|
for term in winners:
|
|
rect = layout[term]
|
|
if util.get_nav_tiebreak(direction, cursor_x, cursor_y,
|
|
rect):
|
|
next = terminals.index(term)
|
|
break;
|
|
else:
|
|
err('Unknown navigation direction: %s' % direction)
|
|
|
|
if next is not None:
|
|
terminals[next].grab_focus()
|
|
|
|
def create_layout(self, layout):
|
|
"""Apply any config items from our layout"""
|
|
if not layout.has_key('children'):
|
|
err('layout describes no children: %s' % layout)
|
|
return
|
|
children = layout['children']
|
|
if len(children) != 1:
|
|
# We're a Window, we can only have one child
|
|
err('incorrect number of children for Window: %s' % layout)
|
|
return
|
|
|
|
child = children[children.keys()[0]]
|
|
terminal = self.get_children()[0]
|
|
dbg('Making a child of type: %s' % child['type'])
|
|
if child['type'] == 'VPaned':
|
|
self.split_axis(terminal, True)
|
|
elif child['type'] == 'HPaned':
|
|
self.split_axis(terminal, False)
|
|
elif child['type'] == 'Notebook':
|
|
self.tab_new()
|
|
i = 2
|
|
while i < len(child['children']):
|
|
self.tab_new()
|
|
i = i + 1
|
|
elif child['type'] == 'Terminal':
|
|
pass
|
|
else:
|
|
err('unknown child type: %s' % child['type'])
|
|
return
|
|
|
|
self.get_children()[0].create_layout(child)
|
|
|
|
if layout.has_key('last_active_term') and layout['last_active_term'] not in ['', None]:
|
|
self.last_active_term = make_uuid(layout['last_active_term'])
|
|
|
|
if layout.has_key('last_active_window') and layout['last_active_window'] == 'True':
|
|
self.terminator.last_active_window = self.uuid
|
|
|
|
class WindowTitle(object):
|
|
"""Class to handle the setting of the window title"""
|
|
|
|
window = None
|
|
text = None
|
|
forced = None
|
|
|
|
def __init__(self, window):
|
|
"""Class initialiser"""
|
|
self.window = window
|
|
self.forced = False
|
|
|
|
def set_title(self, widget, text):
|
|
"""Set the title"""
|
|
if not self.forced:
|
|
self.text = text
|
|
self.update()
|
|
|
|
def force_title(self, newtext):
|
|
"""Force a specific title"""
|
|
if newtext:
|
|
self.set_title(None, newtext)
|
|
self.forced = True
|
|
else:
|
|
self.forced = False
|
|
|
|
def update(self):
|
|
"""Update the title automatically"""
|
|
title = None
|
|
|
|
# FIXME: What the hell is this for?!
|
|
if self.forced:
|
|
title = self.text
|
|
else:
|
|
title = "%s" % self.text
|
|
|
|
self.window.set_title(title)
|
|
|
|
# vim: set expandtab ts=4 sw=4:
|