361 lines
12 KiB
Python
Executable File
361 lines
12 KiB
Python
Executable File
#!/usr/bin/python
|
|
# Terminator by Chris Jones <cmsj@tenshu.net>
|
|
# GPL v2 only
|
|
"""notebook.py - classes for the notebook widget"""
|
|
|
|
import gobject
|
|
import gtk
|
|
|
|
from terminator import Terminator
|
|
from config import Config
|
|
from factory import Factory
|
|
from container import Container
|
|
from editablelabel import EditableLabel
|
|
from translation import _
|
|
from util import err, dbg, get_top_window
|
|
|
|
class Notebook(Container, gtk.Notebook):
|
|
"""Class implementing a gtk.Notebook container"""
|
|
window = None
|
|
|
|
def __init__(self, window):
|
|
"""Class initialiser"""
|
|
if isinstance(window.get_child(), gtk.Notebook):
|
|
err('There is already a Notebook at the top of this window')
|
|
raise(ValueError)
|
|
|
|
Container.__init__(self)
|
|
gtk.Notebook.__init__(self)
|
|
self.terminator = Terminator()
|
|
self.window = window
|
|
gobject.type_register(Notebook)
|
|
self.register_signals(Notebook)
|
|
self.configure()
|
|
|
|
child = window.get_child()
|
|
window.remove(child)
|
|
window.add(self)
|
|
self.newtab(child)
|
|
|
|
self.show_all()
|
|
|
|
def configure(self):
|
|
"""Apply widget-wide settings"""
|
|
# FIXME: Should all of our widgets have a ::configure()?
|
|
|
|
# FIXME: The old reordered handler updated Terminator.terminals with
|
|
# the new order of terminals. We probably need to preserve this for
|
|
# navigation to next/prev terminals.
|
|
|
|
#self.connect('page-reordered', self.on_page_reordered)
|
|
self.set_property('homogeneous', not self.config['scroll_tabbar'])
|
|
self.set_scrollable(self.config['scroll_tabbar'])
|
|
|
|
pos = getattr(gtk, 'POS_%s' % self.config['tab_position'].upper())
|
|
self.set_tab_pos(pos)
|
|
self.set_show_tabs(not self.config['hide_tabbar'])
|
|
|
|
def split_axis(self, widget, vertical=True, sibling=None):
|
|
"""Split the axis of a terminal inside us"""
|
|
page_num = self.page_num(widget)
|
|
if page_num == -1:
|
|
err('Notebook::split_axis: %s not found in Notebook' % widget)
|
|
return
|
|
|
|
label = self.get_tab_label(widget)
|
|
self.remove(widget)
|
|
|
|
maker = Factory()
|
|
if vertical:
|
|
container = maker.make('vpaned')
|
|
else:
|
|
container = maker.make('hpaned')
|
|
|
|
if not sibling:
|
|
sibling = maker.make('terminal')
|
|
sibling.spawn_child()
|
|
|
|
self.insert_page(container, None, page_num)
|
|
self.set_tab_label(container, label)
|
|
self.show_all()
|
|
|
|
container.add(widget)
|
|
container.add(sibling)
|
|
self.set_current_page(page_num)
|
|
|
|
self.show_all()
|
|
|
|
def add(self, widget):
|
|
"""Add a widget to the container"""
|
|
self.newtab(widget)
|
|
|
|
def remove(self, widget):
|
|
"""Remove a widget from the container"""
|
|
page_num = self.page_num(widget)
|
|
if page_num == -1:
|
|
err('Notebook::remove: %s not found in Notebook. Actual parent is: %s' %
|
|
(widget, widget.get_parent()))
|
|
return(False)
|
|
self.remove_page(page_num)
|
|
self.disconnect_child(widget)
|
|
return(True)
|
|
|
|
def newtab(self, widget=None):
|
|
"""Add a new tab, optionally supplying a child widget"""
|
|
top_window = get_top_window(self)
|
|
|
|
if not widget:
|
|
maker = Factory()
|
|
widget = maker.make('Terminal')
|
|
widget.spawn_child()
|
|
|
|
signals = {'close-term': self.wrapcloseterm,
|
|
'split-horiz': self.split_horiz,
|
|
'split-vert': self.split_vert,
|
|
'title-change': self.propagate_title_change,
|
|
'unzoom': self.unzoom}
|
|
|
|
maker = Factory()
|
|
if maker.isinstance(widget, 'Terminal'):
|
|
for signal in signals:
|
|
self.connect_child(widget, signal, signals[signal])
|
|
self.connect_child(widget, 'tab-change', top_window.tab_change)
|
|
self.connect_child(widget, 'group-all', top_window.group_all)
|
|
self.connect_child(widget, 'ungroup-all', top_window.ungroup_all)
|
|
self.connect_child(widget, 'group-tab', top_window.group_tab)
|
|
self.connect_child(widget, 'ungroup-tab', top_window.ungroup_tab)
|
|
|
|
self.set_tab_reorderable(widget, True)
|
|
label = TabLabel(self.window.get_title(), self)
|
|
label.connect('close-clicked', self.closetab)
|
|
|
|
label.show_all()
|
|
widget.show_all()
|
|
|
|
self.append_page(widget, None)
|
|
self.set_tab_label(widget, label)
|
|
self.set_tab_label_packing(widget, not self.config['scroll_tabbar'],
|
|
not self.config['scroll_tabbar'],
|
|
gtk.PACK_START)
|
|
|
|
self.set_current_page(-1)
|
|
widget.grab_focus()
|
|
|
|
def wrapcloseterm(self, widget):
|
|
"""A child terminal has closed"""
|
|
dbg('Notebook::wrapcloseterm: called on %s' % widget)
|
|
if self.closeterm(widget):
|
|
dbg('Notebook::wrapcloseterm: closeterm succeeded')
|
|
self.hoover()
|
|
else:
|
|
dbg('Notebook::wrapcloseterm: closeterm failed')
|
|
|
|
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 xrange(0, nb.get_n_pages()):
|
|
if label == nb.get_tab_label(nb.get_nth_page(i)):
|
|
tabnum = i
|
|
break
|
|
|
|
if not tabnum:
|
|
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')
|
|
child.close()
|
|
elif maker.isinstance(child, 'Container'):
|
|
dbg('Notebook::closetab: child is a Container')
|
|
dialog = self.construct_confirm_close(self.window, _('tab'))
|
|
result = dialog.run()
|
|
dialog.destroy()
|
|
|
|
if result == gtk.RESPONSE_ACCEPT:
|
|
containers = []
|
|
objects = []
|
|
for descendant in child.get_children():
|
|
if maker.isinstance(descendant, 'Container'):
|
|
containers.append(descendant)
|
|
elif maker.isinstance(descendant, 'Terminal'):
|
|
objects.append(descendant)
|
|
|
|
while len(containers) > 0:
|
|
child = containers.pop()
|
|
for descendant in child.get_children():
|
|
if maker.isinstance(descendant, 'Container'):
|
|
containers.append(descendant)
|
|
elif maker.isinstance(descendant, 'Terminal'):
|
|
objects.append(descendant)
|
|
|
|
while len(objects) > 0:
|
|
descendant = objects.pop()
|
|
descendant.close()
|
|
# FIXME: Is this mainloop iterations stuff necessary?
|
|
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
|
|
|
|
nb.remove_page(tabnum)
|
|
del(label)
|
|
|
|
def resizeterm(self, widget, keyname):
|
|
"""Handle a keyboard event requesting a terminal resize"""
|
|
raise NotImplementedError('resizeterm')
|
|
|
|
def zoom(self, widget, fontscale = False):
|
|
"""Zoom a terminal"""
|
|
raise NotImplementedError('zoom')
|
|
|
|
def unzoom(self, widget):
|
|
"""Unzoom a terminal"""
|
|
raise NotImplementedError('unzoom')
|
|
|
|
def find_tab_root(self, widget):
|
|
"""Look for the tab child which is or ultimately contains the supplied
|
|
widget"""
|
|
parent = widget.get_parent()
|
|
previous = parent
|
|
|
|
while parent is not None and parent is not self:
|
|
previous = parent
|
|
parent = parent.get_parent()
|
|
|
|
if previous == self:
|
|
return(widget)
|
|
else:
|
|
return(previous)
|
|
|
|
def update_tab_label_text(self, widget, text):
|
|
"""Update the text of a tab label"""
|
|
notebook = self.find_tab_root(widget)
|
|
label = self.get_tab_label(notebook)
|
|
if not label:
|
|
err('Notebook::update_tab_label_text: %s not found' % widget)
|
|
return
|
|
|
|
label.set_label(text)
|
|
|
|
def hoover(self):
|
|
"""Clean up any empty tabs and if we only have one tab left, die"""
|
|
numpages = self.get_n_pages()
|
|
while numpages > 0:
|
|
numpages = numpages - 1
|
|
page = self.get_nth_page(numpages)
|
|
if not page:
|
|
dbg('Removing empty page: %d' % numpages)
|
|
self.remove_page(numpages)
|
|
|
|
if self.get_n_pages() == 1:
|
|
dbg('Last page, removing self')
|
|
child = self.get_nth_page(0)
|
|
self.remove_page(0)
|
|
parent = self.get_parent()
|
|
parent.remove(self)
|
|
parent.add(child)
|
|
del(self)
|
|
|
|
class TabLabel(gtk.HBox):
|
|
"""Class implementing a label widget for Notebook tabs"""
|
|
notebook = None
|
|
terminator = None
|
|
config = None
|
|
label = None
|
|
icon = None
|
|
button = None
|
|
|
|
__gsignals__ = {
|
|
'close-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
|
(gobject.TYPE_OBJECT,)),
|
|
}
|
|
|
|
def __init__(self, title, notebook):
|
|
"""Class initialiser"""
|
|
gtk.HBox.__init__(self)
|
|
self.__gobject_init__()
|
|
|
|
self.notebook = notebook
|
|
self.terminator = Terminator()
|
|
self.config = Config()
|
|
|
|
self.label = EditableLabel(title)
|
|
self.update_angle()
|
|
|
|
self.pack_start(self.label, True, True)
|
|
|
|
self.update_button()
|
|
self.show_all()
|
|
|
|
def set_label(self, text):
|
|
"""Update the text of our label"""
|
|
self.label.set_text(text)
|
|
|
|
def update_button(self):
|
|
"""Update the state of our close button"""
|
|
if not self.config['close_button_on_tab']:
|
|
if self.button:
|
|
self.button.remove(self.icon)
|
|
self.remove(self.button)
|
|
del(self.button)
|
|
del(self.icon)
|
|
self.button = None
|
|
self.icon = None
|
|
return
|
|
|
|
if not self.button:
|
|
self.button = gtk.Button()
|
|
if not self.icon:
|
|
self.icon = gtk.Image()
|
|
self.icon.set_from_stock(gtk.STOCK_CLOSE,
|
|
gtk.ICON_SIZE_MENU)
|
|
|
|
self.button.set_relief(gtk.RELIEF_NONE)
|
|
self.button.set_focus_on_click(False)
|
|
# FIXME: Why on earth are we doing this twice?
|
|
self.button.set_relief(gtk.RELIEF_NONE)
|
|
self.button.add(self.icon)
|
|
self.button.connect('clicked', self.on_close)
|
|
self.button.set_name('terminator-tab-close-button')
|
|
self.button.connect('style-set', self.on_style_set)
|
|
if hasattr(self.button, 'set_tooltip_text'):
|
|
self.button.set_tooltip_text(_('Close Tab'))
|
|
self.pack_start(self.button, False, False)
|
|
self.show_all()
|
|
|
|
def update_angle(self):
|
|
"""Update the angle of a label"""
|
|
position = self.notebook.get_tab_pos()
|
|
if position == gtk.POS_LEFT:
|
|
self.label.set_angle(90)
|
|
elif position == gtk.POS_RIGHT:
|
|
self.label.set_angle(270)
|
|
else:
|
|
self.label.set_angle(0)
|
|
|
|
def on_style_set(self, widget, prevstyle):
|
|
"""Style changed, recalculate icon size"""
|
|
x, y = gtk.icon_size_lookup_for_settings(self.button.get_settings(),
|
|
gtk.ICON_SIZE_MENU)
|
|
self.button.set_size_request(x + 2, y + 2)
|
|
|
|
def on_close(self, widget):
|
|
"""The close button has been clicked. Destroy the tab"""
|
|
self.emit('close-clicked', self)
|
|
|
|
# vim: set expandtab ts=4 sw=4:
|