From d38a42b580acb3bcecb670f78fdfcb2d25b9afcd Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Fri, 27 Jun 2008 00:24:52 +0100 Subject: [PATCH] split Terminator and TerminatorTerm into their own library files --- terminator | 1707 +----------------------------- terminatorlib/config.py | 2 +- terminatorlib/terminator.py | 833 +++++++++++++++ terminatorlib/terminatorterm.py | 1729 +++++++++++++++++++++++++++++++ 4 files changed, 2567 insertions(+), 1704 deletions(-) create mode 100755 terminatorlib/terminator.py create mode 100755 terminatorlib/terminatorterm.py diff --git a/terminator b/terminator index d00e1e29..9c976d49 100755 --- a/terminator +++ b/terminator @@ -18,7 +18,7 @@ """Terminator by Chris Jones """ # import standard python libs -import os, platform, sys, string, time, math, subprocess +import os, sys, string, time, math, subprocess from optparse import OptionParser #import version details @@ -36,31 +36,9 @@ except: # import unix-lib import pwd -TARGET_TYPE_VTE = 8 - -# import our configuration loader -from terminatorlib import config +# import some useful functions from terminatorlib.config import dbg, err -#import encoding list -from terminatorlib.encoding import TerminatorEncoding - -# Sort out cwd detection code, if available -pid_get_cwd = lambda pid: None -if platform.system() == 'FreeBSD': - try: - from terminatorlib import freebsd - pid_get_cwd = lambda pid: freebsd.get_process_cwd(pid) - dbg ('Using FreeBSD pid_get_cwd') - except: - dbg ('FreeBSD version too old for pid_get_cwd') - pass -elif platform.system() == 'Linux': - dbg ('Using Linux pid_get_cwd') - pid_get_cwd = lambda pid: os.path.realpath ('/proc/%s/cwd' % pid) -else: - dbg ('Unable to set a pid_get_cwd, unknown system: %s'%platform.system) - # import gtk libs # check just in case anyone runs it on a non-gnome system. try: @@ -70,1685 +48,8 @@ except: "gobject, gtk and pango to run Terminator.")) sys.exit(1) -# import a library for viewing URLs -try: - # gnome.url_show() is really useful - import gnome - url_show = gnome.url_show -except: - # webbrowser.open() is not really useful, but will do as a fallback - import webbrowser - url_show = webbrowser.open - -# import vte-bindings -try: - import vte -except: - error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, - _('You need to install python bindings for libvte ("python-vte" in debian/ubuntu)')) - error.run() - sys.exit (1) - -def openurl (url): - try: - if subprocess.call(["xdg-open", url]) != 0: - raise - except: - try: - url_show (url) - except: - pass - -class TerminatorTerm (gtk.VBox): - - matches = {} - - def __init__ (self, terminator, profile = None, command = None, cwd = None): - gtk.VBox.__init__ (self) - self.terminator = terminator - self.conf = terminator.conf - self.command = command - - self.cwd = cwd or os.getcwd(); - if not os.path.exists(self.cwd) or not os.path.isdir(self.cwd): - self.cwd = pwd.getpwuid(os.getuid ())[5] - - self.clipboard = gtk.clipboard_get (gtk.gdk.SELECTION_CLIPBOARD) - self.scrollbar_position = self.conf.scrollbar_position - - self._vte = vte.Terminal () - self._vte.set_size (80, 24) - self.reconfigure_vte () - self._vte.show () - - self._termbox = gtk.HBox () - self._termbox.show() - self._title = gtk.Label() - self._title.show() - self._titlebox = gtk.EventBox() - self._titlebox.add(self._title) - self.show() - self.pack_start(self._titlebox, False) - self.pack_start(self._termbox) - - if self.conf.titlebars: - self._titlebox.show() - else: - self._titlebox.hide() - - self._scrollbar = gtk.VScrollbar (self._vte.get_adjustment ()) - if self.scrollbar_position != "hidden" and self.scrollbar_position != "disabled": - self._scrollbar.show () - - if self.scrollbar_position == 'left': - packfunc = self._termbox.pack_end - else: - packfunc = self._termbox.pack_start - - packfunc (self._vte) - packfunc (self._scrollbar, False) - - self._vte.connect ("key-press-event", self.on_vte_key_press) - self._vte.connect ("button-press-event", self.on_vte_button_press) - self._vte.connect ("popup-menu", self.on_vte_popup_menu) - - """drag and drop""" - srcvtetargets = [ ( "vte", gtk.TARGET_SAME_APP, TARGET_TYPE_VTE ) ] - dsttargets = [ ( "vte", gtk.TARGET_SAME_APP, TARGET_TYPE_VTE ), ('text/plain', 0, 0) , ("STRING", 0, 0), ("COMPOUND_TEXT", 0, 0)] - self._vte.drag_source_set( gtk.gdk.CONTROL_MASK | gtk.gdk.BUTTON3_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) - self._titlebox.drag_source_set( gtk.gdk.BUTTON1_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) - #self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) - self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) - self._vte.connect("drag-begin", self.on_drag_begin, self) - self._titlebox.connect("drag-begin", self.on_drag_begin, self) - self._vte.connect("drag-data-get", self.on_drag_data_get, self) - self._titlebox.connect("drag-data-get", self.on_drag_data_get, self) - #for testing purpose: drag-motion - self._vte.connect("drag-motion", self.on_drag_motion, self) - self._vte.connect("drag-data-received", self.on_drag_data_received, self) - - if self.conf.copy_on_selection: - self._vte.connect ("selection-changed", lambda widget: self._vte.copy_clipboard ()) - - self._vte.connect ("composited-changed", self.on_composited_changed) - self._vte.connect ("window-title-changed", self.on_vte_title_change) - self._vte.connect ("grab-focus", self.on_vte_focus) - self._vte.connect ("focus-out-event", self.on_vte_focus_out) - self._vte.connect ("focus-in-event", self.on_vte_focus_in) - - - exit_action = self.conf.exit_action - if exit_action == "restart": - self._vte.connect ("child-exited", self.spawn_child) - # We need to support "left" because some buggy versions of gnome-terminal - # set it in some situations - elif exit_action in ("close", "left"): - self._vte.connect ("child-exited", lambda close_term: self.terminator.closeterm (self)) - - self._vte.add_events (gtk.gdk.ENTER_NOTIFY_MASK) - self._vte.connect ("enter_notify_event", self.on_vte_notify_enter) - - self.add_matches() - - dbg ('SEGBUG: Setting http_proxy') - env_proxy = os.getenv ('http_proxy') - if not env_proxy and self.conf.http_proxy and self.conf.http_proxy != '': - os.putenv ('http_proxy', self.conf.http_proxy) - - dbg ('SEGBUG: Setting COLORTERM') - os.putenv ('COLORTERM', 'gnome-terminal') - dbg ('SEGBUG: TerminatorTerm __init__ complete') - - def on_drag_begin(self, widget, drag_context, data): - dbg ('Drag begins') - widget.drag_source_set_icon_pixbuf(self.terminator.icon_theme.load_icon (APP_NAME, 48, 0)) - - def on_drag_data_get(self,widget, drag_context, selection_data, info, time, data): - dbg ("Drag data get") - selection_data.set("vte",info, str(data.terminator.term_list.index (self))) - - - def on_drag_motion(self, widget, drag_context, x, y, time, data): - dbg ("Drag Motion on ") - """ -x-special/gnome-icon-list -text/uri-list -UTF8_STRING -COMPOUND_TEXT -TEXT -STRING -text/plain;charset=utf-8 -text/plain;charset=UTF-8 -text/plain - """ - - if 'text/plain' in drag_context.targets: - #copy text from another widget - return - srcwidget = drag_context.get_source_widget() - if (isinstance(srcwidget, gtk.EventBox) and srcwidget == self._titlebox) or widget == srcwidget: - #on self - return - - - alloc = widget.allocation - rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height) - widget.window.invalidate_rect(rect, True) - widget.window.process_updates(True) - - context = widget.window.cairo_create() - if self.conf.use_theme_colors: - color = self._vte.get_style ().text[gtk.STATE_NORMAL] - else: - color = gtk.gdk.color_parse (self.conf.foreground_color) - - - context.set_source_rgba(color.red, color.green, color.blue, 0.5) - - pos = self.get_location(widget, x, y) - topleft = (0,0) - topright = (alloc.width,0) - topmiddle = (alloc.width/2,0) - bottomleft = (0, alloc.height) - bottomright = (alloc.width,alloc.height) - bottommiddle = (alloc.width/2, alloc.height) - middle = (alloc.width/2, alloc.height/2) - middleleft = (0, alloc.height/2) - middleright = (alloc.width, alloc.height/2) - #print "%f %f %d %d" %(coef1, coef2, b1,b2) - coord = () - if pos == "right": - coord = (topright, topmiddle, bottommiddle, bottomright) - if pos == "top": - coord = (topleft, topright, middleright , middleleft) - if pos == "left": - coord = (topleft, topmiddle, bottommiddle, bottomleft) - if pos == "bottom": - coord = (bottomleft, bottomright, middleright , middleleft) - - if len(coord) > 0 : - context.move_to(coord[len(coord)-1][0],coord[len(coord)-1][1]) - for i in coord: - context.line_to(i[0],i[1]) - - context.fill() - - - def on_drag_drop(self, widget, drag_context, x, y, time): - parent = widget.get_parent() - dbg ('Drag drop on %s'%parent) - - def on_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time, data): - dbg ("Drag Data Received") - if selection_data.type == 'text/plain': - #copy text to destination - #print "%s %s" % (selection_data.type, selection_data.target) - txt = selection_data.data.strip() - if txt[0:7] == "file://": - txt = "'%s'" % txt[7:] - self._vte.feed_child(txt) - return - - widgetsrc = data.terminator.term_list[int(selection_data.data)] - srcvte = drag_context.get_source_widget() - #check if computation requireds - if (isinstance(srcvte, gtk.EventBox) and srcvte == self._titlebox) or srcvte == widget: - dbg (" on itself") - return - - srchbox = widgetsrc - dsthbox = widget.get_parent().get_parent() - - dstpaned = dsthbox.get_parent() - srcpaned = srchbox.get_parent() - if isinstance(dstpaned, gtk.Window) and isinstance(srcpaned, gtk.Window): - dbg (" Only one terminal") - return - pos = self.get_location(widget, x, y) - - data.terminator.remove(widgetsrc) - data.terminator.add(self, widgetsrc,pos) - return - - - def get_location(self, vte, x, y): - pos = "" - #get the diagonales function for the receiving widget - coef1 = float(vte.allocation.height)/float(vte.allocation.width) - coef2 = -float(vte.allocation.height)/float(vte.allocation.width) - b1 = 0 - b2 = vte.allocation.height - #determine position in rectangle - """ - -------- - |\ /| - | \ / | - | \/ | - | /\ | - | / \ | - |/ \| - -------- - """ - if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ): - pos = "right" - if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ): - pos = "top" - if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ): - pos = "left" - if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ): - pos = "bottom" - return pos - - - def add_matches (self, lboundry="[[:<:]]", rboundry="[[:>:]]"): - userchars = "-A-Za-z0-9" - passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" - hostchars = "-A-Za-z0-9" - pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'" - schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" - user = "[" + userchars + "]+(:[" + passchars + "]+)?" - urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" - - self.matches['full_uri'] = self._vte.match_add(lboundry + schemes + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") - - # FreeBSD works with [[:<:]], Linux works with \< - if self.matches['full_uri'] == -1: - if lboundry != "\\<": - self.add_matches(lboundry = "\\<", rboundry = "\\>") - else: - self.matches['addr_only'] = self._vte.match_add (lboundry + "(www|ftp)[" + hostchars + "]*\.[" + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") - self.matches['email'] = self._vte.match_add (lboundry + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+[.a-zA-Z0-9-]*" + rboundry) - self.matches['nntp'] = self._vte.match_add (lboundry + '''news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@[-A-Za-z0-9.]+(:[0-9]+)?''' + rboundry) - - def spawn_child (self, event=None): - update_records = self.conf.update_records - login = self.conf.login_shell - args = [] - shell = '' - - if self.command: - dbg ('spawn_child: using self.command: %s'%self.command) - args = self.command - shell = self.command[0] - elif self.conf.use_custom_command: - dbg ('spawn_child: using custom command: %s'%self.conf.custom_command) - args = self.conf.custom_command.split () - shell = args[0] - - try: - if os.environ['PATH'] == "": - raise (ValueError) - paths = os.environ['PATH'].split(':') - except: - paths = ['/usr/local/bin', '/usr/bin', '/bin'] - dbg ('spawn_child: found paths: "%s"'%paths) - - if self.conf.use_custom_command and shell[0] != '/': - for path in paths: - dbg ('spawn_child: looking for pathless custom command "%s"'%os.path.join (path, shell)) - if os.path.exists (os.path.join (path, shell)): - shell = os.path.join (path, shell) - break - - if not self.command and not os.path.exists (shell): - dbg ('spawn_child: hunting for a command') - shell = os.getenv ('SHELL') or '' - args = [] - if not os.path.exists (shell): - dbg ('spawn_child: No usable shell in $SHELL (%s)'%os.getenv('SHELL')) - shell = pwd.getpwuid (os.getuid ())[6] or '' - if not os.path.exists (shell): - for i in ['bash','zsh','tcsh','ksh','csh','sh']: - for p in paths: - shell = os.path.join(p, i) - dbg ('spawn_child: Checking if "%s" exists'%shell) - if not os.path.exists (shell): - dbg ('spawn_child: %s does not exist'%shell) - continue - else: - dbg ('spawn_child: %s does exist'%shell) - break - if os.path.exists (shell): - break - - if not self.command and not os.path.exists (shell): - # Give up, we're completely stuck - err (_('Unable to find a shell')) - gobject.timeout_add (100, self.terminator.closeterm, self) - return (-1) - - if not args: - args.append (shell) - - if self.conf.login_shell: - args[0] = "-%s"%args[0] - - dbg ('SEGBUG: Setting WINDOWID') - os.putenv ('WINDOWID', '%s'%self._vte.get_parent_window().xid) - - dbg ('SEGBUG: Forking command: "%s" with args "%s", loglastlog = "%s", logwtmp = "%s", logutmp = "%s" and cwd "%s"'%(shell, args, login, update_records, update_records, self.cwd)) - self._pid = self._vte.fork_command (command = shell, argv = args, envv = [], loglastlog = login, logwtmp = update_records, logutmp = update_records, directory=self.cwd) - - dbg ('SEGBUG: Forked command') - if self._pid == -1: - err (_('Unable to start shell: ') + shell) - return (-1) - - def get_cwd (self): - """ Return the current working directory of the subprocess. - This function requires OS specific behaviours - """ - cwd = pid_get_cwd (self._pid) - dbg ('get_cwd found: %s'%cwd) - return (cwd) - - def reconfigure_vte (self): - # Set our emulation - self._vte.set_emulation (self.conf.emulation) - - # Set our wordchars - self._vte.set_word_chars (self.conf.word_chars) - - # Set our mouselation - self._vte.set_mouse_autohide (self.conf.mouse_autohide) - - # Set our compatibility - backspace = self.conf.backspace_binding - delete = self.conf.delete_binding - -# Note, each of the 4 following comments should replace the line beneath it, but the python-vte bindings don't appear to support this constant, so the magic values are being assumed from the C enum :/ - if backspace == "ascii-del": -# backbind = vte.ERASE_ASCII_BACKSPACE - backbind = 2 - else: -# backbind = vte.ERASE_AUTO_BACKSPACE - backbind = 1 - - if delete == "escape-sequence": -# delbind = vte.ERASE_DELETE_SEQUENCE - delbind = 3 - else: -# delbind = vte.ERASE_AUTO - delbind = 0 - - self._vte.set_backspace_binding (backbind) - self._vte.set_delete_binding (delbind) - - # Set our font - try: - self._vte.set_font (pango.FontDescription (self.conf.font)) - except: - pass - - # Set our boldness - self._vte.set_allow_bold (self.conf.allow_bold) - - # Set our color scheme - palette = self.conf.palette - if self.conf.use_theme_colors: - fg_color = self._vte.get_style ().text[gtk.STATE_NORMAL] - bg_color = self._vte.get_style ().base[gtk.STATE_NORMAL] - else: - fg_color = gtk.gdk.color_parse (self.conf.foreground_color) - bg_color = gtk.gdk.color_parse (self.conf.background_color) - - colors = palette.split (':') - palette = [] - for color in colors: - if color: - palette.append (gtk.gdk.color_parse (color)) - self._vte.set_colors (fg_color, bg_color, palette) - - # Set our background image, transparency and type - # Many thanks to the authors of gnome-terminal, on which this code is based. - background_type = self.conf.background_type - - # set background image settings - if background_type == "image": - self._vte.set_background_image_file (self.conf.background_image) - self._vte.set_scroll_background (self.conf.scroll_background) - else: - self._vte.set_background_image_file('') - self._vte.set_scroll_background(False) - - # set transparency for the background (image) - if background_type in ("image", "transparent"): - self._vte.set_background_tint_color (bg_color) - self._vte.set_background_saturation(1 - (self.conf.background_darkness)) - self._vte.set_opacity(int(self.conf.background_darkness * 65535)) - else: - self._vte.set_background_saturation(1) - self._vte.set_opacity(65535) - - if not self._vte.is_composited(): - self._vte.set_background_transparent (background_type == "transparent") - else: - self._vte.set_background_transparent (False) - - # Set our cursor blinkiness - self._vte.set_cursor_blinks = (self.conf.cursor_blink) - - # Set our audible belliness - silent_bell = self.conf.silent_bell - self._vte.set_audible_bell (not silent_bell) - - # Set our visual flashiness - self._vte.set_visible_bell (silent_bell) - - # Override our flashybelliness - if self.conf.force_no_bell: - self._vte.set_visible_bell (False) - self._vte.set_audible_bell (False) - - # Set our scrolliness - self._vte.set_scrollback_lines (self.conf.scrollback_lines) - self._vte.set_scroll_on_keystroke (self.conf.scroll_on_keystroke) - self._vte.set_scroll_on_output (self.conf.scroll_on_output) - - if self.scrollbar_position != self.conf.scrollbar_position: - self.scrollbar_position = self.conf.scrollbar_position - - if self.scrollbar_position == 'hidden' or self.scrollbar_position == 'disabled': - self._scrollbar.hide () - else: - self._scrollbar.show () - if self.scrollbar_position == 'right': - self._termbox.reorder_child (self._vte, 0) - elif self.scrollbar_position == 'left': - self._termbox.reorder_child (self._scrollbar, 0) - - # Set our sloppiness - self.focus = self.conf.focus - - self._vte.queue_draw () - - def on_composited_changed (self, widget): - self.reconfigure_vte () - - def on_vte_button_press (self, term, event): - # Left mouse button + Ctrl while over a link should open it - mask = gtk.gdk.CONTROL_MASK - if (event.state & mask) == mask: - if event.button == 1: - url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) - if url: - if (url[0][0:7] != "mailto:") & (url[1] == self.matches['email']): - address = "mailto:" + url[0] - else: - address = url[0] - openurl ( address ) - return False - - # Left mouse button should transfer focus to this vte widget - #LP#242612: - # we also need to give focus on the widget where the paste occured - if event.button in (1 ,2): - self._vte.grab_focus () - return False - - # Right mouse button should display a context menu if ctrl not pressed - if event.button == 3 and event.state & gtk.gdk.CONTROL_MASK == 0: - self.do_popup (event) - return True - - def on_vte_notify_enter (self, term, event): - if (self.focus == "sloppy" or self.focus == "mouse"): - term.grab_focus () - return False - - def do_scrollbar_toggle (self): - self.toggle_widget_visibility (self._scrollbar) - - def do_title_toggle (self): - self.toggle_widget_visibility (self._titlebox) - - def toggle_widget_visibility (self, widget): - if not isinstance (widget, gtk.Widget): - raise TypeError - - if widget.get_property ('visible'): - widget.hide () - else: - widget.show () - - - def paste_clipboard(self): - self._vte.paste_clipboard() - self._vte.grab_focus() - - - #keybindings for the individual splited terminals (affects only the - #the selected terminal) - def on_vte_key_press (self, term, event): - keyname = gtk.gdk.keyval_name (event.keyval) - - mask = gtk.gdk.CONTROL_MASK - if (event.state & mask) == mask: - if keyname == 'plus': - self.zoom (True) - return (True) - elif keyname == 'minus': - self.zoom (False) - return (True) - elif keyname == 'equal': - self.zoom_orig () - return (True) - - mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK - if (event.state & mask) == mask: - #Top level tab - if keyname == 'T': - self.terminator.newtab (self, True) - return (True) - # bindings that should be moved to Terminator as they all just call - # a function of Terminator. It would be cleaner is TerminatorTerm - # has absolutely no reference to Terminator. - # N (next) - P (previous) - O (horizontal) - E (vertical) - W (close) - - mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK - if (event.state & mask) == mask: - if keyname == 'N': - self.terminator.go_next (self) - return (True) - elif keyname == "P": - self.terminator.go_prev (self) - return (True) - elif keyname == 'O': - self.terminator.splitaxis (self, False) - return (True) - elif keyname == 'E': - self.terminator.splitaxis (self, True) - return (True) - elif keyname == 'W': - self.terminator.closeterm (self) - return (True) - elif keyname == 'C': - self._vte.copy_clipboard () - return (True) - elif keyname == 'V': - self.paste_clipboard () - return (True) - elif keyname == 'S': - self.do_scrollbar_toggle () - return (True) - elif keyname == 'T': - self.terminator.newtab(self) - return (True) - elif keyname in ('Up', 'Down', 'Left', 'Right'): - self.terminator.resizeterm (self, keyname) - return (True) - elif keyname == 'Page_Down': - self.terminator.move_tab(self, 'right') - return (True) - elif keyname == 'Page_Up': - self.terminator.move_tab(self, 'left') - return (True) - elif keyname == 'Z': - self.terminator.toggle_zoom (self, True) - return (True) - elif keyname == 'X': - self.terminator.toggle_zoom (self) - return (True) - - mask = gtk.gdk.CONTROL_MASK - if (event.state & mask) == mask: - if keyname == 'Page_Down': - self.terminator.next_tab(self) - return (True) - elif keyname == 'Page_Up': - self.terminator.previous_tab(self) - return (True) - - if keyname and (keyname == 'Tab' or keyname.endswith('_Tab')): - mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK - if (event.state & mask) == mask: - self.terminator.go_prev (self) - return (True) - mask = gtk.gdk.CONTROL_MASK - if (event.state & mask) == mask: - self.terminator.go_next (self) - return (True) - # Warning, mask value is either gtk.gdk.CONTROL_MASK or gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK - # if you intend to use it, reinit it - return (False) - - def zoom_orig (self): - self._vte.set_font (pango.FontDescription (self.conf.font)) - - def zoom (self, zoom_in): - pangodesc = self._vte.get_font () - fontsize = pangodesc.get_size () - - if fontsize > pango.SCALE and not zoom_in: - fontsize -= pango.SCALE - elif zoom_in: - fontsize += pango.SCALE - - pangodesc.set_size (fontsize) - self._vte.set_font (pangodesc) - - def on_vte_popup_menu (self, term, event): - self.do_popup (event) - - def do_popup (self, event = None): - menu = self.create_popup_menu (event) - menu.popup (None, None, None, event.button, event.time) - - def create_popup_menu (self, event): - menu = gtk.Menu () - url = None - - if event: - url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) - - if url: - if url[1] != self.matches['email']: - address = url[0] - nameopen = _("_Open Link") - namecopy = _("_Copy Link Address") - iconopen = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) - - item = gtk.ImageMenuItem (nameopen) - item.set_property('image', iconopen) - else: - if url[0][0:7] != "mailto:": - address = "mailto:" + url[0] - else: - address = url[0] - nameopen = _("_Send Mail To...") - namecopy = _("_Copy Email Address") - - item = gtk.MenuItem (nameopen) - - item.connect ("activate", lambda menu_item: openurl (address)) - menu.append (item) - - item = gtk.MenuItem (namecopy) - item.connect ("activate", lambda menu_item: self.clipboard.set_text (url[0])) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_COPY) - item.connect ("activate", lambda menu_item: self._vte.copy_clipboard ()) - item.set_sensitive (self._vte.get_has_selection ()) - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_PASTE) - item.connect ("activate", lambda menu_item: self.paste_clipboard ()) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.CheckMenuItem (_("Show _scrollbar")) - item.set_active (self._scrollbar.get_property ('visible')) - item.connect ("toggled", lambda menu_item: self.do_scrollbar_toggle ()) - menu.append (item) - - item = gtk.CheckMenuItem (_("Show _titlebar")) - item.set_active (self._titlebox.get_property ('visible')) - item.connect ("toggled", lambda menu_item: self.do_title_toggle ()) - menu.append (item) - - self._do_encoding_items (menu) - - item = gtk.MenuItem () - menu.append (item) - - if not self.terminator._zoomed: - str_horiz = _("Split H_orizontally") - str_vert = _("Split V_ertically") - - item = gtk.ImageMenuItem (str_horiz) - item_image = gtk.Image () - item_image.set_from_icon_name (APP_NAME + '_horiz', gtk.ICON_SIZE_MENU) - item.set_image (item_image) - - item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, False)) - menu.append (item) - - item = gtk.ImageMenuItem (str_vert) - item_image = gtk.Image () - item_image.set_from_icon_name (APP_NAME + '_vert', gtk.ICON_SIZE_MENU) - item.set_image (item_image) - - item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, True)) - menu.append (item) - - item = gtk.MenuItem (_("Open _Tab")) - item.connect ("activate", lambda menu_item: self.terminator.newtab (self)) - menu.append (item) - - if self.conf.extreme_tabs: - item = gtk.MenuItem (_("Open Top Level Tab")) - item.connect ("activate", lambda menu_item: self.terminator.newtab (self, True)) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - if len (self.terminator.term_list) > 1: - if not self.terminator._zoomed: - item = gtk.MenuItem (_("_Zoom terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) - menu.append (item) - - item = gtk.MenuItem (_("_Maximise terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) - menu.append (item) - else: - if self.terminator._zoomed and not self.terminator._maximised: - item = gtk.MenuItem (_("_Unzoom terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) - menu.append (item) - - if self.terminator._zoomed and self.terminator._maximised: - item = gtk.MenuItem (_("U_nmaximise terminal")) - item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) - menu.append (item) - - item = gtk.MenuItem () - menu.append (item) - - item = gtk.ImageMenuItem (gtk.STOCK_CLOSE) - item.connect ("activate", lambda menu_item: self.terminator.closeterm (self)) - menu.append (item) - - menu.show_all () - return menu - - def on_encoding_change (self, widget, encoding): - current = self._vte.get_encoding () - if current != encoding: - dbg ('Setting Encoding to: %s'%encoding) - self._vte.set_encoding (encoding) - - def _do_encoding_items (self, menu): - active_encodings = self.conf.active_encodings - item = gtk.MenuItem (_("Encodings")) - menu.append (item) - submenu = gtk.Menu () - item.set_submenu (submenu) - - current_encoding = self._vte.get_encoding () - group = None - for encoding in active_encodings: - radioitem = gtk.RadioMenuItem (group, _(encoding)) - if group is None: - group = radioitem - - if encoding == current_encoding: - radioitem.set_active (True) - - radioitem.connect ('activate', self.on_encoding_change, encoding) - submenu.append (radioitem) - - item = gtk.MenuItem (_("Other Encodings")) - submenu.append (item) - #second level - - submenu = gtk.Menu () - item.set_submenu (submenu) - encodings = TerminatorEncoding ().get_list () - encodings.sort (lambda x, y: cmp (x[2].lower (), y[2].lower ())) - group = None - - for encoding in encodings: - if encoding[1] in active_encodings: - continue - - if encoding[1] is None: - label = "%s %s"%(encoding[2], self._vte.get_encoding ()) - else: - label = "%s %s"%(encoding[2], encoding[1]) - - radioitem = gtk.RadioMenuItem (group, label) - if group is None: - group = radioitem - - if encoding[1] == current_encoding: - radioitem.set_active (True) - - radioitem.connect ('activate', self.on_encoding_change, encoding[1]) - submenu.append (radioitem) - - def on_vte_title_change(self, vte): - if self.conf.titletips: - vte.set_property ("has-tooltip", True) - vte.set_property ("tooltip-text", vte.get_window_title ()) - #set the title anyhow, titlebars setting only show/hide the label - self._title.set_text(vte.get_window_title ()) - self.terminator.set_window_title("%s: %s" %(APP_NAME.capitalize(), vte.get_window_title ())) - notebookpage = self.terminator.get_first_notebook_page(vte) - while notebookpage != None: - notebookpage[0].set_tab_label_text(notebookpage[1], vte.get_window_title ()) - notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) - - def on_vte_focus_in(self, vte, event): - self._titlebox.modify_bg(gtk.STATE_NORMAL,self.terminator.window.get_style().bg[gtk.STATE_SELECTED]) - self._title.modify_fg(gtk.STATE_NORMAL, self.terminator.window.get_style().fg[gtk.STATE_SELECTED]) - return - - def on_vte_focus_out(self, vte, event): - self._titlebox.modify_bg(gtk.STATE_NORMAL, self.terminator.window.get_style().bg[gtk.STATE_NORMAL]) - self._title.modify_fg(gtk.STATE_NORMAL, self.terminator.window.get_style().fg[gtk.STATE_NORMAL]) - return - - def on_vte_focus(self, vte): - if vte.get_window_title (): - self.terminator.set_window_title("%s: %s" %(APP_NAME.capitalize(), vte.get_window_title ())) - notebookpage = self.terminator.get_first_notebook_page(vte) - while notebookpage != None: - notebookpage[0].set_tab_label_text(notebookpage[1], vte.get_window_title ()) - notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) - - def destroy(self): - self._vte.destroy() - -class Terminator: - def __init__ (self, profile = None, command = None, fullscreen = False, maximise = False, borderless = False): - self.profile = profile - self.command = command - - self._zoomed = False - self._maximised = False - self._fullscreen = False - self._f11_modifier = False - self.term_list = [] - stores = [] - stores.append (config.TerminatorConfValuestoreRC ()) - - try: - import gconf - if self.profile: - self.profile = gconf.escape_key (self.profile, -1) - store = config.TerminatorConfValuestoreGConf (self.profile) - store.set_reconfigure_callback (self.reconfigure_vtes) - dbg ('Terminator__init__: comparing %s and %s'%(self.profile, store.profile.split ('/').pop ())) - if self.profile == store.profile.split ('/').pop (): - # If we have been given a profile, and we loaded it, we should be higher priority than RC - dbg ('Terminator__init__: placing GConf before RC') - stores.insert (0, store) - else: - stores.append (store) - except: - pass - - self.conf = config.TerminatorConfig (stores) - - self.icon_theme = gtk.IconTheme () - - if self.conf.f11_modifier: - self._f11_modifier = True - - if self.conf.handle_size in range (0,6): - gtk.rc_parse_string(""" - style "terminator-paned-style" { - GtkPaned::handle_size = %s - } - - class "GtkPaned" style "terminator-paned-style" - """ % self.conf.handle_size) - self.window = gtk.Window () - self.window.set_title (APP_NAME.capitalize()) - - try: - self.window.set_icon (self.icon_theme.load_icon (APP_NAME, 48, 0)) - except: - self.icon = self.window.render_icon (gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON) - self.window.set_icon (self.icon) - - self.window.connect ("key-press-event", self.on_key_press) - self.window.connect ("delete_event", self.on_delete_event) - self.window.connect ("destroy", self.on_destroy_event) - self.window.connect ("window-state-event", self.on_window_state_changed) - - self.window.set_property ('allow-shrink', True) - - if fullscreen or self.conf.fullscreen: - self.fullscreen_toggle () - - if maximise or self.conf.maximise: - self.maximize () - - if borderless or self.conf.borderless: - self.window.set_decorated (False) - - # Set RGBA colormap if possible so VTE can use real alpha - # channels for transparency. - screen = self.window.get_screen() - colormap = screen.get_rgba_colormap() - if colormap: - self.window.set_colormap(colormap) - - # Start out with just one terminal - # FIXME: This should be really be decided from some kind of profile - term = (TerminatorTerm (self, self.profile, self.command)) - self.term_list = [term] - - self.window.add (term) - term._titlebox.hide() - self.window.show () - term.spawn_child () - - def maximize (self): - """ Maximize the Terminator window.""" - self.window.maximize () - - def fullscreen_toggle (self): - """ Toggle the fullscreen state of the window. If it is in - fullscreen state, it will be unfullscreened. If it is not, it - will be set to fullscreen state. - """ - if self._fullscreen: - self.window.unfullscreen () - else: - self.window.fullscreen () - - def on_window_state_changed (self, window, event): - state = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN - self._fullscreen = bool (state) - - return (False) - - def on_delete_event (self, window, event, data=None): - if len (self.term_list) == 1: - return False - - # show dialog - dialog = gtk.Dialog (_("Close?"), window, gtk.DIALOG_MODAL, - (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) - dialog.set_has_separator (False) - dialog.set_resizable (False) - - primairy = gtk.Label (_('Close all terminals?')) - primairy.set_use_markup (True) - primairy.set_alignment (0, 0.5) - secundairy = gtk.Label (_("This window has %s terminals open. Closing the window will also close all terminals.") % len(self.term_list)) - secundairy.set_line_wrap(True) - primairy.set_alignment (0, 0.5) - - labels = gtk.VBox () - labels.pack_start (primairy, False, False, 6) - labels.pack_start (secundairy, False, False, 6) - - image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) - image.set_alignment (0.5, 0) - - box = gtk.HBox() - box.pack_start (image, False, False, 6) - box.pack_start (labels, False, False, 6) - dialog.vbox.pack_start (box, False, False, 12) - - dialog.show_all () - result = dialog.run () - dialog.destroy () - return not (result == gtk.RESPONSE_ACCEPT) - - def on_destroy_event (self, widget, data=None): - gtk.main_quit () - - # keybindings for the whole terminal window (affects the main - # windows containing the splited terminals) - def on_key_press (self, window, event): - """ Callback for the window to determine what to do with special - keys. Currently handled key-combo's: - * F11: toggle fullscreen state of the window. - * CTRL - SHIFT - Q: close all terminals - """ - keyname = gtk.gdk.keyval_name (event.keyval) - mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK - - if (keyname == 'F11' and (self._f11_modifier == False or event.state & mask)): - self.fullscreen_toggle () - return (True) - - if (event.state & mask) == mask: - if keyname == 'Q': - if not self.on_delete_event (window, gtk.gdk.Event (gtk.gdk.DELETE)): - self.on_destroy_event (window, gtk.gdk.Event (gtk.gdk.DESTROY)) - - def set_window_title(self, title): - """ - Modifies Terminator window title - """ - self.window.set_title(title) - - def add(self, widget, terminal, pos = "bottom"): - """ - Add a term to another at position pos - """ - vertical = pos in ("top", "bottom") - pane = (vertical) and gtk.VPaned () or gtk.HPaned () - - # get the parent of the provided terminal - parent = widget.get_parent () - dbg ('SEGBUG: add() Got parent') - if isinstance (parent, gtk.Window): - dbg ('SEGBUG: parent is a gtk.Window') - # We have just one term - parent.remove(widget) - dbg ('SEGBUG: removed widget from window') - if pos in ("top", "left"): - dbg ('SEGBUG: pos is in top/left') - pane.pack1 (terminal, True, True) - dbg ('SEGBUG: packed terminal') - pane.pack2 (widget, True, True) - dbg ('SEGBUG: packed widget') - else: - dbg ('SEGBUG: pos is not in top/left') - pane.pack1 (widget, True, True) - dbg ('SEGBUG: packed widget') - pane.pack2 (terminal, True, True) - dbg ('SEGBUG: packed terminal') - parent.add (pane) - dbg ('SEGBUG: added pane to parent') - - position = (vertical) and parent.allocation.height \ - or parent.allocation.width - - if (isinstance (parent, gtk.Notebook) or isinstance (parent, gtk.Window)) and widget.conf.titlebars: - #not the only term in the notebook/window anymore, need to reshow the title - dbg ('SEGBUG: Showing _titlebox') - widget._titlebox.show() - - if isinstance (parent, gtk.Notebook): - dbg ('SEGBUG: Parent is a notebook') - page = -1 - - for i in range(0, parent.get_n_pages()): - if parent.get_nth_page(i) == widget: - page = i - break - widget.reparent (pane) - if pos in ("top", "left"): - pane.remove(widget) - pane.pack1 (terminal, True, True) - pane.pack2 (widget, True, True) - else: - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - #parent.remove_page(page) - pane.show() - parent.insert_page(pane, None, page) - parent.set_tab_label_text(pane, widget._vte.get_window_title()) - parent.set_tab_label_packing(pane, True, True, gtk.PACK_START) - parent.set_tab_reorderable(pane, True) - parent.set_current_page(page) - - position = (vertical) and parent.allocation.height \ - or parent.allocation.width - - if isinstance (parent, gtk.Paned): - dbg ('SEGBUG: parent is a Paned') - # We are inside a split term - position = (vertical) and widget.allocation.height \ - or widget.allocation.width - - dbg ('SEGBUG: Preparing to reparent sibling') - if (widget == parent.get_child1 ()): - widget.reparent (pane) - parent.pack1 (pane, True, True) - else: - widget.reparent (pane) - parent.pack2 (pane, True, True) - - if pos in ("top", "left"): - dbg ('SEGBUG: pos is in top/left. Removing and re-ordering') - pane.remove(widget) - pane.pack1 (terminal, True, True) - pane.pack2 (widget, True, True) - else: - dbg ('SEGBUG: pos is not in top/left. Packing') - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - - dbg ('SEGBUG: packing widget and terminal') - pane.pack1 (widget, True, True) - pane.pack2 (terminal, True, True) - dbg ('SEGBUG: packed widget and terminal') - - # show all, set position of the divider - dbg ('SEGBUG: Showing pane') - pane.show () - dbg ('SEGBUG: Showed pane') - pane.set_position (position / 2) - dbg ('SEGBUG: Set position') - terminal.show () - - # insert the term reference into the list - index = self.term_list.index (widget) - if pos in ('bottom', 'right'): - index = index + 1 - self.term_list.insert (index, terminal) - # make the new terminal grab the focus - terminal._vte.grab_focus () - - return (terminal) - - def on_page_reordered(self, notebook, child, page_num): - #page has been reordered, we need to get the - # first term and last term - dbg ("Reordered: %d"%page_num) - nbpages = notebook.get_n_pages() - if nbpages == 1: - dbg("[ERROR] only one page in on_page_reordered") - - first = self._notebook_first_term(notebook.get_nth_page(page_num)) - last = self._notebook_last_term(notebook.get_nth_page(page_num)) - firstidx = self.term_list.index(first) - lastidx = self.term_list.index(last) - termslice = self.term_list[firstidx:lastidx+1] - #remove them from the list - for term in termslice: - self.term_list.remove(term) - - if page_num == 0: - #first page, we insert before the first term of next page - nexttab = notebook.get_nth_page(1) - sibling = self._notebook_first_term(nexttab) - siblingindex = self.term_list.index(sibling) - for term in termslice: - self.term_list.insert(siblingindex, term) - siblingindex += 1 - else: - #other pages, we insert after the last term of previous page - previoustab = notebook.get_nth_page(page_num - 1) - sibling = self._notebook_last_term(previoustab) - siblingindex = self.term_list.index(sibling) - for term in termslice: - siblingindex += 1 - self.term_list.insert(siblingindex, term) - - #for page reorder, we need to get the first term of a notebook - def notebook_first_term(self, notebook): - return self._notebook_first_term(notebook.get_nth_page(0)) - - def _notebook_first_term(self, child): - if isinstance(child, TerminatorTerm): - return child - elif isinstance(child, gtk.Paned): - return self._notebook_first_term(child.get_child1()) - elif isinstance(child, gtk.Notebook): - return self._notebook_first_term(child.get_nth_page(0)) - - dbg("[ERROR] unsupported class %s in _notebook_first_term" % child.__class__.__name__) - return None - - #for page reorder, we need to get the last term of a notebook - def notebook_last_term(self, notebook): - return self._notebook_last_term(notebook.get_nth_page(notebook.get_n_pages()-1)) - - def _notebook_last_term(self, child): - if isinstance(child, TerminatorTerm): - return child - elif isinstance(child, gtk.Paned): - return self._notebook_last_term(child.get_child2()) - elif isinstance(child, gtk.Notebook): - return self._notebook_last_term(child.get_nth_page(child.get_n_pages()-1)) - - dbg("[ERROR] unsupported class %s in _notebook_last_term" % child.__class__.__name__) - return None - - def newtab(self,widget, toplevel = False): - if self._zoomed: - # We don't want to add a new tab while we are zoomed in on a terminal - dbg ("newtab function called, but Terminator was in zoomed terminal mode.") - return - - terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) - #only one term, we don't show the title - terminal._titlebox.hide() - if self.conf.extreme_tabs and not toplevel: - parent = widget.get_parent () - child = widget - else: - child = self.window.get_children()[0] - parent = child.get_parent() - - if isinstance(parent, gtk.Paned) or (isinstance(parent, gtk.Window) - and - ((self.conf.extreme_tabs and not toplevel) or not isinstance(child, gtk.Notebook))): - #no notebook yet. - notebook = gtk.Notebook() - notebook.set_tab_pos(gtk.POS_TOP) - notebook.connect('page-reordered',self.on_page_reordered) - notebook.set_property('homogeneous', True) - notebook.set_tab_reorderable(widget, True) - - if isinstance(parent, gtk.Paned): - if parent.get_child1() == child: - child.reparent(notebook) - parent.pack1(notebook) - else: - child.reparent(notebook) - parent.pack2(notebook) - elif isinstance(parent, gtk.Window): - child.reparent(notebook) - parent.add(notebook) - notebook.set_tab_reorderable(child,True) - notebooklabel = "" - if isinstance(child, TerminatorTerm): - child._titlebox.hide() - if widget._vte.get_window_title() is not None: - notebooklabel = widget._vte.get_window_title() - notebook.set_tab_label_text(child, notebooklabel) - notebook. set_tab_label_packing(child, True, True, gtk.PACK_START) - notebook.show() - elif isinstance(parent, gtk.Notebook): - notebook = parent - elif isinstance(parent, gtk.Window) and isinstance(child, gtk.Notebook): - notebook = child - else: - return (False) - - ## NOTE - ## Here we need to append to the notebook before we can - ## spawn the terminal (WINDOW_ID needs to be set) - - notebook.append_page(terminal,None) - terminal.show () - terminal.spawn_child () - ## Some gtk/vte weirdness - ## If we don't use this silly test, - ## terminal._vte.get_window_title() might return - ## bogus values - notebooklabel = "" - if terminal._vte.get_window_title() is not None: - notebooklabel = terminal._vte.get_window_title() - notebook.set_tab_label_text(terminal, notebooklabel) - notebook.set_tab_label_packing(terminal, True, True, gtk.PACK_START) - notebook.set_tab_reorderable(terminal,True) - ## Now, we set focus on the new term - notebook.set_current_page(-1) - terminal._vte.grab_focus () - - #adding a new tab, thus we need to get the - # last term of the previous tab and add - # the new term just after - sibling = self._notebook_last_term(notebook.get_nth_page(notebook.page_num(terminal)-1)) - index = self.term_list.index(sibling) - self.term_list.insert (index + 1, terminal) - return (True) - - - return terminal - - def splitaxis (self, widget, vertical=True): - """ Split the provided widget on the horizontal or vertical axis. """ - if self._zoomed: - # We don't want to split the terminal while we are in zoomed mode - dbg ("splitaxis function called, but Terminator was in zoomed mode.") - return - - # create a new terminal and parent pane. - dbg ('SEGBUG: Creating TerminatorTerm') - terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) - dbg ('SEGBUG: Created TerminatorTerm') - pos = vertical and "right" or "bottom" - dbg ('SEGBUG: Position is: %s'%pos) - self.add(widget, terminal, pos) - dbg ('SEGBUG: added TerminatorTerm to container') - terminal.show () - dbg ('SEGBUG: showed TerminatorTerm') - terminal.spawn_child () - dbg ('SEGBUG: spawned child') - return terminal - - def remove(self, widget): - """Remove a TerminatorTerm from the Terminator view and terms list - Returns True on success, False on failure""" - parent = widget.get_parent () - sibling = None - focus_on_close = 'prev' - if isinstance (parent, gtk.Window): - # We are the only term - if not self.on_delete_event (parent, gtk.gdk.Event (gtk.gdk.DELETE)): - self.on_destroy_event (parent, gtk.gdk.Event (gtk.gdk.DESTROY)) - return - - if isinstance (parent, gtk.Paned): - index = self.term_list.index (widget) - grandparent = parent.get_parent () - - # Discover sibling while all objects exist - if widget == parent.get_child1 (): - sibling = parent.get_child2 () - focus_on_close = 'next' - if widget == parent.get_child2 (): - sibling = parent.get_child1 () - - if not sibling: - # something is wrong, give up - err ("Error: %s is not a child of %s"%(widget, parent)) - return False - - parent.remove(widget) - if isinstance(grandparent, gtk.Notebook): - page = -1 - for i in range(0, grandparent.get_n_pages()): - if grandparent.get_nth_page(i) == parent: - page = i - break - parent.remove(sibling) - grandparent.remove_page(page) - grandparent.insert_page(sibling, None,page) - grandparent.set_tab_label_packing(sibling, True, True, gtk.PACK_START) - grandparent.set_tab_reorderable(sibling, True) - grandparent.set_current_page(page) - - else: - grandparent.remove (parent) - sibling.reparent (grandparent) - if not self._zoomed: - grandparent.resize_children() - parent.destroy () - if isinstance(sibling, TerminatorTerm) and isinstance(sibling.get_parent(), gtk.Notebook): - sibling._titlebox.hide() - - self.term_list.remove (widget) - - elif isinstance (parent, gtk.Notebook): - parent.remove(widget) - nbpages = parent.get_n_pages() - index = self.term_list.index (widget) - self.term_list.remove (widget) - if nbpages == 1: - sibling = parent.get_nth_page(0) - parent.remove(sibling) - gdparent = parent.get_parent() - if isinstance(gdparent, gtk.Window): - gdparent.remove(parent) - gdparent.add(sibling) - elif isinstance(gdparent, gtk.Paned): - if gdparent.get_child1() == parent: - gdparent.remove(parent) - gdparent.pack1(sibling) - else: - gdparent.remove(parent) - gdparent.pack2(sibling) - if isinstance(sibling, TerminatorTerm) and sibling.conf.titlebars and sibling.conf.extreme_tabs: - sibling._titlebox.show() - parent.destroy() - if self.conf.focus_on_close == 'prev' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'prev'): - if index == 0: index = 1 - self.term_list[index - 1]._vte.grab_focus () - self._set_current_notebook_page_recursive(self.term_list[index - 1]) - elif self.conf.focus_on_close == 'next' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'next'): - if index == len(self.term_list): index = index - 1 - self.term_list[index]._vte.grab_focus () - self._set_current_notebook_page_recursive(self.term_list[index]) - - if len(self.term_list) == 1: - self.term_list[0]._titlebox.hide() - - return True - - def closeterm (self, widget): - if self._zoomed: - # We are zoomed, pop back out to normal layout before closing - dbg ("closeterm function called while in zoomed mode. Restoring previous layout before closing.") - self.toggle_zoom(widget, not self._maximised) - - if self.remove(widget): - widget.destroy () - return True - return False - - - def go_next (self, term): - current = self.term_list.index (term) - next = None - if self.conf.cycle_term_tab: - notebookpage = self.get_first_notebook_page(term) - if notebookpage: - last = self._notebook_last_term(notebookpage[1]) - first = self._notebook_first_term(notebookpage[1]) - if term == last: - next = self.term_list.index(first) - - if next is None: - if current == len (self.term_list) - 1: - next = 0 - else: - next = current + 1 - - - nextterm = self.term_list[next] - ##we need to set the current page of each notebook - self._set_current_notebook_page_recursive(nextterm) - - nextterm._vte.grab_focus () - - - def go_prev (self, term): - current = self.term_list.index (term) - previous = None - - if self.conf.cycle_term_tab: - notebookpage = self.get_first_notebook_page(term) - if notebookpage: - last = self._notebook_last_term(notebookpage[1]) - first = self._notebook_first_term(notebookpage[1]) - if term == first: - previous = self.term_list.index(last) - - if previous is None: - if current == 0: - previous = len (self.term_list) - 1 - else: - previous = current - 1 - - #self.window.set_title(self.term_list[previous]._vte.get_window_title()) - previousterm = self.term_list[previous] - ##we need to set the current page of each notebook - self._set_current_notebook_page_recursive(previousterm) - previousterm._vte.grab_focus () - - - def _set_current_notebook_page_recursive(self, widget): - page = self.get_first_notebook_page(widget) - while page: - child = None - page_num = page[0].page_num(page[1]) - page[0].set_current_page(page_num) - page = self.get_first_notebook_page(page[0]) - - - def resizeterm (self, widget, keyname): - vertical = False - if keyname in ('Up', 'Down'): - vertical = True - elif keyname in ('Left', 'Right'): - vertical = False - else: - return - parent = self.get_first_parent_paned(widget,vertical) - if parent == None: - return - - #We have a corresponding parent pane - # - #allocation = parent.get_allocation() - - if keyname in ('Up', 'Down'): - maxi = parent.get_child1().get_allocation().height + parent.get_child2().get_allocation().height - 1 - - else: - maxi = parent.get_child1().get_allocation().width + parent.get_child2().get_allocation().width - 1 - move = 10 - if keyname in ('Up', 'Left'): - move = -10 - - move = max(2, parent.get_position() + move) - move = min(maxi, move) - - parent.set_position(move) - - def previous_tab(self, term): - notebook = self.get_first_parent_notebook(term) - notebook.prev_page() - return - - def next_tab(self, term): - notebook = self.get_first_parent_notebook(term) - notebook.next_page() - return - - def move_tab(self, term, direction): - dbg("moving to direction %s" % direction) - (notebook, page) = self.get_first_notebook_page(term) - page_num = notebook.page_num(page) - nbpages = notebook.get_n_pages() - #dbg ("%s %s %s %s" % (page_num, nbpages,notebook, page)) - if page_num == 0 and direction == 'left': - new_page_num = nbpages - elif page_num == nbpages - 1 and direction == 'right': - new_page_num = 0 - elif direction == 'left': - new_page_num = page_num - 1 - elif direction == 'right': - new_page_num = page_num + 1 - else: - dbg("[ERROR] unhandled combination in move_tab: direction = %s page_num = %d" % (direction, page_num)) - return False - notebook.reorder_child(page, new_page_num) - return True - - def get_first_parent_notebook(self, widget): - if isinstance (widget, gtk.Window): - return None - parent = widget.get_parent() - if isinstance (parent, gtk.Notebook): - return parent - return self.get_first_parent_notebook(parent) - - def get_first_parent_paned (self, widget, vertical = None): - """This method returns the first parent pane of a widget. - if vertical is True returns the first VPaned - if vertical is False return the first Hpaned - if is None return the First Paned""" - if isinstance (widget, gtk.Window): - return None - parent = widget.get_parent() - if isinstance (parent, gtk.Paned) and vertical is None: - return parent - if isinstance (parent, gtk.VPaned) and vertical: - return parent - elif isinstance (parent, gtk.HPaned) and not vertical: - return parent - return self.get_first_parent_paned(parent, vertical) - - def get_first_notebook_page(self, widget): - if isinstance (widget, gtk.Window): - return None - parent = widget.get_parent() - if isinstance (parent, gtk.Notebook): - page = -1 - for i in range(0, parent.get_n_pages()): - if parent.get_nth_page(i) == widget: - return (parent, widget) - return self.get_first_notebook_page(parent) - - def reconfigure_vtes (self): - for term in self.term_list: - term.reconfigure_vte () - - def toggle_zoom(self, widget, fontscale = False): - if not self._zoomed: - widget._titlebars = widget._titlebox.get_property ('visible') - dbg ('toggle_zoom: not zoomed. remembered titlebar setting of %s'%widget._titlebars) - if widget._titlebars: - widget._titlebox.hide() - self.zoom_term (widget, fontscale) - else: - dbg ('toggle_zoom: zoomed. restoring titlebar setting of %s'%widget._titlebars) - self.unzoom_term (widget, True) - if widget._titlebars and \ - len(self.term_list) > 1 \ - and \ - (isinstance(widget, TerminatorTerm) and isinstance(widget.get_parent(),gtk.Paned))\ - : - widget._titlebox.show() - widget._vte.grab_focus() - - def zoom_term (self, widget, fontscale = False): - """Maximize to full window an instance of TerminatorTerm.""" - self.old_font = widget._vte.get_font () - self.old_columns = widget._vte.get_column_count () - self.old_rows = widget._vte.get_row_count () - self.old_parent = widget.get_parent() - - if isinstance(self.old_parent, gtk.Window): - return - if isinstance(self.old_parent, gtk.Notebook): - self.old_page = self.old_parent.get_current_page() - - self.window_child = self.window.get_children()[0] - self.window.remove(self.window_child) - self.old_parent.remove(widget) - self.window.add(widget) - self._zoomed = True - - if fontscale: - self.cnid = widget.connect ("size-allocate", self.zoom_scale_font) - else: - self._maximised = True - - widget._vte.grab_focus () - - def zoom_scale_font (self, widget, allocation): - new_columns = widget._vte.get_column_count () - new_rows = widget._vte.get_row_count () - new_font = widget._vte.get_font () - - dbg ('zoom_scale_font: I just went from %dx%d to %dx%d. Raa!'%(self.old_columns, self.old_rows, new_columns, new_rows)) - - if new_rows != self.old_rows: - titleheight = widget._titlebox.get_allocation().height - vtecharheight = widget._vte.get_char_height() - rowdiff = new_rows - self.old_rows + 2 - dbg ('zoom_scale_font: titlebox height is %d, char_height is %d'%(titleheight, vtecharheight)) - dbg ('zoom_scale_font: lhs: %d, rhs: %f'%((titleheight / vtecharheight), rowdiff)) - care_height = (rowdiff <= vtecharheight / rowdiff) - dbg ('zoom_scale_font: caring about height difference: %s'%care_height) - else: - care_height = False - - if (new_rows <= self.old_rows) or care_height or (new_columns <= self.old_columns): - dbg ('zoom_scale_font: Which means I didnt scale on one axis (col: %s, row: %s). Bailing'%((new_columns <= self.old_columns), (new_rows <= self.old_rows))) - return - - old_area = self.old_columns * self.old_rows - new_area = new_columns * new_rows - area_factor = new_area / old_area - - dbg ('zoom_scale_font: My area changed from %d characters to %d characters, a factor of %f.'%(old_area, new_area, area_factor)) - - new_font.set_size (self.old_font.get_size() * (area_factor / 2)) - dbg ('zoom_scale_font: Scaled font from %f to %f'%(self.old_font.get_size () / pango.SCALE, new_font.get_size () / pango.SCALE)) - widget._vte.set_font (new_font) - widget.disconnect (self.cnid) - - def unzoom_term (self, widget, fontscale = False): - """Proof of concept: Go back to previous application - widget structure. - """ - if self._zoomed: - if fontscale: - widget._vte.set_font (self.old_font) - self._zoomed = False - self._maximised = False - - self.window.remove(widget) - self.window.add(self.window_child) - self.old_parent.add(widget) - if isinstance(self.old_parent, gtk.Notebook): - self.old_parent.set_current_page(self.old_page) - - widget._vte.grab_focus () +#import Terminator +from terminatorlib.terminator import Terminator if __name__ == '__main__': def execute_cb (option, opt, value, parser): diff --git a/terminatorlib/config.py b/terminatorlib/config.py index ace9951d..ec983cf4 100755 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -38,7 +38,7 @@ import os, sys, re import pwd # set this to true to enable debugging output -debug = True +debug = False def dbg (log = ""): if debug: diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py new file mode 100755 index 00000000..b1921045 --- /dev/null +++ b/terminatorlib/terminator.py @@ -0,0 +1,833 @@ +#!/usr/bin/python +# Terminator - multiple gnome terminals in one window +# Copyright (C) 2006-2008 cmsj@tenshu.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 2 only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Terminator by Chris Jones """ +import gobject, gtk, pango + +#import version details +from terminatorlib.version import * + +# import our configuration loader +from terminatorlib import config +from terminatorlib.config import dbg, err + +#import TerminatorTerm +from terminatorlib.terminatorterm import TerminatorTerm + +class Terminator: + def __init__ (self, profile = None, command = None, fullscreen = False, maximise = False, borderless = False): + self.profile = profile + self.command = command + + self._zoomed = False + self._maximised = False + self._fullscreen = False + self._f11_modifier = False + self.term_list = [] + stores = [] + stores.append (config.TerminatorConfValuestoreRC ()) + + try: + import gconf + if self.profile: + self.profile = gconf.escape_key (self.profile, -1) + store = config.TerminatorConfValuestoreGConf (self.profile) + store.set_reconfigure_callback (self.reconfigure_vtes) + dbg ('Terminator__init__: comparing %s and %s'%(self.profile, store.profile.split ('/').pop ())) + if self.profile == store.profile.split ('/').pop (): + # If we have been given a profile, and we loaded it, we should be higher priority than RC + dbg ('Terminator__init__: placing GConf before RC') + stores.insert (0, store) + else: + stores.append (store) + except: + pass + + self.conf = config.TerminatorConfig (stores) + + self.icon_theme = gtk.IconTheme () + + if self.conf.f11_modifier: + self._f11_modifier = True + + if self.conf.handle_size in range (0,6): + gtk.rc_parse_string(""" + style "terminator-paned-style" { + GtkPaned::handle_size = %s + } + + class "GtkPaned" style "terminator-paned-style" + """ % self.conf.handle_size) + self.window = gtk.Window () + self.window.set_title (APP_NAME.capitalize()) + + try: + self.window.set_icon (self.icon_theme.load_icon (APP_NAME, 48, 0)) + except: + self.icon = self.window.render_icon (gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON) + self.window.set_icon (self.icon) + + self.window.connect ("key-press-event", self.on_key_press) + self.window.connect ("delete_event", self.on_delete_event) + self.window.connect ("destroy", self.on_destroy_event) + self.window.connect ("window-state-event", self.on_window_state_changed) + + self.window.set_property ('allow-shrink', True) + + if fullscreen or self.conf.fullscreen: + self.fullscreen_toggle () + + if maximise or self.conf.maximise: + self.maximize () + + if borderless or self.conf.borderless: + self.window.set_decorated (False) + + # Set RGBA colormap if possible so VTE can use real alpha + # channels for transparency. + screen = self.window.get_screen() + colormap = screen.get_rgba_colormap() + if colormap: + self.window.set_colormap(colormap) + + # Start out with just one terminal + # FIXME: This should be really be decided from some kind of profile + term = (TerminatorTerm (self, self.profile, self.command)) + self.term_list = [term] + + self.window.add (term) + term._titlebox.hide() + self.window.show () + term.spawn_child () + + def maximize (self): + """ Maximize the Terminator window.""" + self.window.maximize () + + def fullscreen_toggle (self): + """ Toggle the fullscreen state of the window. If it is in + fullscreen state, it will be unfullscreened. If it is not, it + will be set to fullscreen state. + """ + if self._fullscreen: + self.window.unfullscreen () + else: + self.window.fullscreen () + + def on_window_state_changed (self, window, event): + state = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN + self._fullscreen = bool (state) + + return (False) + + def on_delete_event (self, window, event, data=None): + if len (self.term_list) == 1: + return False + + # show dialog + dialog = gtk.Dialog (_("Close?"), window, gtk.DIALOG_MODAL, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) + dialog.set_has_separator (False) + dialog.set_resizable (False) + + primairy = gtk.Label (_('Close all terminals?')) + primairy.set_use_markup (True) + primairy.set_alignment (0, 0.5) + secundairy = gtk.Label (_("This window has %s terminals open. Closing the window will also close all terminals.") % len(self.term_list)) + secundairy.set_line_wrap(True) + primairy.set_alignment (0, 0.5) + + labels = gtk.VBox () + labels.pack_start (primairy, False, False, 6) + labels.pack_start (secundairy, False, False, 6) + + image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) + image.set_alignment (0.5, 0) + + box = gtk.HBox() + box.pack_start (image, False, False, 6) + box.pack_start (labels, False, False, 6) + dialog.vbox.pack_start (box, False, False, 12) + + dialog.show_all () + result = dialog.run () + dialog.destroy () + return not (result == gtk.RESPONSE_ACCEPT) + + def on_destroy_event (self, widget, data=None): + gtk.main_quit () + + # keybindings for the whole terminal window (affects the main + # windows containing the splited terminals) + def on_key_press (self, window, event): + """ Callback for the window to determine what to do with special + keys. Currently handled key-combo's: + * F11: toggle fullscreen state of the window. + * CTRL - SHIFT - Q: close all terminals + """ + keyname = gtk.gdk.keyval_name (event.keyval) + mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK + + if (keyname == 'F11' and (self._f11_modifier == False or event.state & mask)): + self.fullscreen_toggle () + return (True) + + if (event.state & mask) == mask: + if keyname == 'Q': + if not self.on_delete_event (window, gtk.gdk.Event (gtk.gdk.DELETE)): + self.on_destroy_event (window, gtk.gdk.Event (gtk.gdk.DESTROY)) + + def set_window_title(self, title): + """ + Modifies Terminator window title + """ + self.window.set_title(title) + + def add(self, widget, terminal, pos = "bottom"): + """ + Add a term to another at position pos + """ + vertical = pos in ("top", "bottom") + pane = (vertical) and gtk.VPaned () or gtk.HPaned () + + # get the parent of the provided terminal + parent = widget.get_parent () + dbg ('SEGBUG: add() Got parent') + if isinstance (parent, gtk.Window): + dbg ('SEGBUG: parent is a gtk.Window') + # We have just one term + parent.remove(widget) + dbg ('SEGBUG: removed widget from window') + if pos in ("top", "left"): + dbg ('SEGBUG: pos is in top/left') + pane.pack1 (terminal, True, True) + dbg ('SEGBUG: packed terminal') + pane.pack2 (widget, True, True) + dbg ('SEGBUG: packed widget') + else: + dbg ('SEGBUG: pos is not in top/left') + pane.pack1 (widget, True, True) + dbg ('SEGBUG: packed widget') + pane.pack2 (terminal, True, True) + dbg ('SEGBUG: packed terminal') + parent.add (pane) + dbg ('SEGBUG: added pane to parent') + + position = (vertical) and parent.allocation.height \ + or parent.allocation.width + + if (isinstance (parent, gtk.Notebook) or isinstance (parent, gtk.Window)) and widget.conf.titlebars: + #not the only term in the notebook/window anymore, need to reshow the title + dbg ('SEGBUG: Showing _titlebox') + widget._titlebox.show() + + if isinstance (parent, gtk.Notebook): + dbg ('SEGBUG: Parent is a notebook') + page = -1 + + for i in range(0, parent.get_n_pages()): + if parent.get_nth_page(i) == widget: + page = i + break + widget.reparent (pane) + if pos in ("top", "left"): + pane.remove(widget) + pane.pack1 (terminal, True, True) + pane.pack2 (widget, True, True) + else: + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + #parent.remove_page(page) + pane.show() + parent.insert_page(pane, None, page) + parent.set_tab_label_text(pane, widget._vte.get_window_title()) + parent.set_tab_label_packing(pane, True, True, gtk.PACK_START) + parent.set_tab_reorderable(pane, True) + parent.set_current_page(page) + + position = (vertical) and parent.allocation.height \ + or parent.allocation.width + + if isinstance (parent, gtk.Paned): + dbg ('SEGBUG: parent is a Paned') + # We are inside a split term + position = (vertical) and widget.allocation.height \ + or widget.allocation.width + + dbg ('SEGBUG: Preparing to reparent sibling') + if (widget == parent.get_child1 ()): + widget.reparent (pane) + parent.pack1 (pane, True, True) + else: + widget.reparent (pane) + parent.pack2 (pane, True, True) + + if pos in ("top", "left"): + dbg ('SEGBUG: pos is in top/left. Removing and re-ordering') + pane.remove(widget) + pane.pack1 (terminal, True, True) + pane.pack2 (widget, True, True) + else: + dbg ('SEGBUG: pos is not in top/left. Packing') + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + + dbg ('SEGBUG: packing widget and terminal') + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + dbg ('SEGBUG: packed widget and terminal') + + # show all, set position of the divider + dbg ('SEGBUG: Showing pane') + pane.show () + dbg ('SEGBUG: Showed pane') + pane.set_position (position / 2) + dbg ('SEGBUG: Set position') + terminal.show () + + # insert the term reference into the list + index = self.term_list.index (widget) + if pos in ('bottom', 'right'): + index = index + 1 + self.term_list.insert (index, terminal) + # make the new terminal grab the focus + terminal._vte.grab_focus () + + return (terminal) + + def on_page_reordered(self, notebook, child, page_num): + #page has been reordered, we need to get the + # first term and last term + dbg ("Reordered: %d"%page_num) + nbpages = notebook.get_n_pages() + if nbpages == 1: + dbg("[ERROR] only one page in on_page_reordered") + + first = self._notebook_first_term(notebook.get_nth_page(page_num)) + last = self._notebook_last_term(notebook.get_nth_page(page_num)) + firstidx = self.term_list.index(first) + lastidx = self.term_list.index(last) + termslice = self.term_list[firstidx:lastidx+1] + #remove them from the list + for term in termslice: + self.term_list.remove(term) + + if page_num == 0: + #first page, we insert before the first term of next page + nexttab = notebook.get_nth_page(1) + sibling = self._notebook_first_term(nexttab) + siblingindex = self.term_list.index(sibling) + for term in termslice: + self.term_list.insert(siblingindex, term) + siblingindex += 1 + else: + #other pages, we insert after the last term of previous page + previoustab = notebook.get_nth_page(page_num - 1) + sibling = self._notebook_last_term(previoustab) + siblingindex = self.term_list.index(sibling) + for term in termslice: + siblingindex += 1 + self.term_list.insert(siblingindex, term) + + #for page reorder, we need to get the first term of a notebook + def notebook_first_term(self, notebook): + return self._notebook_first_term(notebook.get_nth_page(0)) + + def _notebook_first_term(self, child): + if isinstance(child, TerminatorTerm): + return child + elif isinstance(child, gtk.Paned): + return self._notebook_first_term(child.get_child1()) + elif isinstance(child, gtk.Notebook): + return self._notebook_first_term(child.get_nth_page(0)) + + dbg("[ERROR] unsupported class %s in _notebook_first_term" % child.__class__.__name__) + return None + + #for page reorder, we need to get the last term of a notebook + def notebook_last_term(self, notebook): + return self._notebook_last_term(notebook.get_nth_page(notebook.get_n_pages()-1)) + + def _notebook_last_term(self, child): + if isinstance(child, TerminatorTerm): + return child + elif isinstance(child, gtk.Paned): + return self._notebook_last_term(child.get_child2()) + elif isinstance(child, gtk.Notebook): + return self._notebook_last_term(child.get_nth_page(child.get_n_pages()-1)) + + dbg("[ERROR] unsupported class %s in _notebook_last_term" % child.__class__.__name__) + return None + + def newtab(self,widget, toplevel = False): + if self._zoomed: + # We don't want to add a new tab while we are zoomed in on a terminal + dbg ("newtab function called, but Terminator was in zoomed terminal mode.") + return + + terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) + #only one term, we don't show the title + terminal._titlebox.hide() + if self.conf.extreme_tabs and not toplevel: + parent = widget.get_parent () + child = widget + else: + child = self.window.get_children()[0] + parent = child.get_parent() + + if isinstance(parent, gtk.Paned) or (isinstance(parent, gtk.Window) + and + ((self.conf.extreme_tabs and not toplevel) or not isinstance(child, gtk.Notebook))): + #no notebook yet. + notebook = gtk.Notebook() + notebook.set_tab_pos(gtk.POS_TOP) + notebook.connect('page-reordered',self.on_page_reordered) + notebook.set_property('homogeneous', True) + notebook.set_tab_reorderable(widget, True) + + if isinstance(parent, gtk.Paned): + if parent.get_child1() == child: + child.reparent(notebook) + parent.pack1(notebook) + else: + child.reparent(notebook) + parent.pack2(notebook) + elif isinstance(parent, gtk.Window): + child.reparent(notebook) + parent.add(notebook) + notebook.set_tab_reorderable(child,True) + notebooklabel = "" + if isinstance(child, TerminatorTerm): + child._titlebox.hide() + if widget._vte.get_window_title() is not None: + notebooklabel = widget._vte.get_window_title() + notebook.set_tab_label_text(child, notebooklabel) + notebook. set_tab_label_packing(child, True, True, gtk.PACK_START) + notebook.show() + elif isinstance(parent, gtk.Notebook): + notebook = parent + elif isinstance(parent, gtk.Window) and isinstance(child, gtk.Notebook): + notebook = child + else: + return (False) + + ## NOTE + ## Here we need to append to the notebook before we can + ## spawn the terminal (WINDOW_ID needs to be set) + + notebook.append_page(terminal,None) + terminal.show () + terminal.spawn_child () + ## Some gtk/vte weirdness + ## If we don't use this silly test, + ## terminal._vte.get_window_title() might return + ## bogus values + notebooklabel = "" + if terminal._vte.get_window_title() is not None: + notebooklabel = terminal._vte.get_window_title() + notebook.set_tab_label_text(terminal, notebooklabel) + notebook.set_tab_label_packing(terminal, True, True, gtk.PACK_START) + notebook.set_tab_reorderable(terminal,True) + ## Now, we set focus on the new term + notebook.set_current_page(-1) + terminal._vte.grab_focus () + + #adding a new tab, thus we need to get the + # last term of the previous tab and add + # the new term just after + sibling = self._notebook_last_term(notebook.get_nth_page(notebook.page_num(terminal)-1)) + index = self.term_list.index(sibling) + self.term_list.insert (index + 1, terminal) + return (True) + + + return terminal + + def splitaxis (self, widget, vertical=True): + """ Split the provided widget on the horizontal or vertical axis. """ + if self._zoomed: + # We don't want to split the terminal while we are in zoomed mode + dbg ("splitaxis function called, but Terminator was in zoomed mode.") + return + + # create a new terminal and parent pane. + dbg ('SEGBUG: Creating TerminatorTerm') + terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) + dbg ('SEGBUG: Created TerminatorTerm') + pos = vertical and "right" or "bottom" + dbg ('SEGBUG: Position is: %s'%pos) + self.add(widget, terminal, pos) + dbg ('SEGBUG: added TerminatorTerm to container') + terminal.show () + dbg ('SEGBUG: showed TerminatorTerm') + terminal.spawn_child () + dbg ('SEGBUG: spawned child') + return terminal + + def remove(self, widget): + """Remove a TerminatorTerm from the Terminator view and terms list + Returns True on success, False on failure""" + parent = widget.get_parent () + sibling = None + focus_on_close = 'prev' + if isinstance (parent, gtk.Window): + # We are the only term + if not self.on_delete_event (parent, gtk.gdk.Event (gtk.gdk.DELETE)): + self.on_destroy_event (parent, gtk.gdk.Event (gtk.gdk.DESTROY)) + return + + if isinstance (parent, gtk.Paned): + index = self.term_list.index (widget) + grandparent = parent.get_parent () + + # Discover sibling while all objects exist + if widget == parent.get_child1 (): + sibling = parent.get_child2 () + focus_on_close = 'next' + if widget == parent.get_child2 (): + sibling = parent.get_child1 () + + if not sibling: + # something is wrong, give up + err ("Error: %s is not a child of %s"%(widget, parent)) + return False + + parent.remove(widget) + if isinstance(grandparent, gtk.Notebook): + page = -1 + for i in range(0, grandparent.get_n_pages()): + if grandparent.get_nth_page(i) == parent: + page = i + break + parent.remove(sibling) + grandparent.remove_page(page) + grandparent.insert_page(sibling, None,page) + grandparent.set_tab_label_packing(sibling, True, True, gtk.PACK_START) + grandparent.set_tab_reorderable(sibling, True) + grandparent.set_current_page(page) + + else: + grandparent.remove (parent) + sibling.reparent (grandparent) + if not self._zoomed: + grandparent.resize_children() + parent.destroy () + if isinstance(sibling, TerminatorTerm) and isinstance(sibling.get_parent(), gtk.Notebook): + sibling._titlebox.hide() + + self.term_list.remove (widget) + + elif isinstance (parent, gtk.Notebook): + parent.remove(widget) + nbpages = parent.get_n_pages() + index = self.term_list.index (widget) + self.term_list.remove (widget) + if nbpages == 1: + sibling = parent.get_nth_page(0) + parent.remove(sibling) + gdparent = parent.get_parent() + if isinstance(gdparent, gtk.Window): + gdparent.remove(parent) + gdparent.add(sibling) + elif isinstance(gdparent, gtk.Paned): + if gdparent.get_child1() == parent: + gdparent.remove(parent) + gdparent.pack1(sibling) + else: + gdparent.remove(parent) + gdparent.pack2(sibling) + if isinstance(sibling, TerminatorTerm) and sibling.conf.titlebars and sibling.conf.extreme_tabs: + sibling._titlebox.show() + parent.destroy() + if self.conf.focus_on_close == 'prev' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'prev'): + if index == 0: index = 1 + self.term_list[index - 1]._vte.grab_focus () + self._set_current_notebook_page_recursive(self.term_list[index - 1]) + elif self.conf.focus_on_close == 'next' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'next'): + if index == len(self.term_list): index = index - 1 + self.term_list[index]._vte.grab_focus () + self._set_current_notebook_page_recursive(self.term_list[index]) + + if len(self.term_list) == 1: + self.term_list[0]._titlebox.hide() + + return True + + def closeterm (self, widget): + if self._zoomed: + # We are zoomed, pop back out to normal layout before closing + dbg ("closeterm function called while in zoomed mode. Restoring previous layout before closing.") + self.toggle_zoom(widget, not self._maximised) + + if self.remove(widget): + widget.destroy () + return True + return False + + + def go_next (self, term): + current = self.term_list.index (term) + next = None + if self.conf.cycle_term_tab: + notebookpage = self.get_first_notebook_page(term) + if notebookpage: + last = self._notebook_last_term(notebookpage[1]) + first = self._notebook_first_term(notebookpage[1]) + if term == last: + next = self.term_list.index(first) + + if next is None: + if current == len (self.term_list) - 1: + next = 0 + else: + next = current + 1 + + + nextterm = self.term_list[next] + ##we need to set the current page of each notebook + self._set_current_notebook_page_recursive(nextterm) + + nextterm._vte.grab_focus () + + + def go_prev (self, term): + current = self.term_list.index (term) + previous = None + + if self.conf.cycle_term_tab: + notebookpage = self.get_first_notebook_page(term) + if notebookpage: + last = self._notebook_last_term(notebookpage[1]) + first = self._notebook_first_term(notebookpage[1]) + if term == first: + previous = self.term_list.index(last) + + if previous is None: + if current == 0: + previous = len (self.term_list) - 1 + else: + previous = current - 1 + + #self.window.set_title(self.term_list[previous]._vte.get_window_title()) + previousterm = self.term_list[previous] + ##we need to set the current page of each notebook + self._set_current_notebook_page_recursive(previousterm) + previousterm._vte.grab_focus () + + + def _set_current_notebook_page_recursive(self, widget): + page = self.get_first_notebook_page(widget) + while page: + child = None + page_num = page[0].page_num(page[1]) + page[0].set_current_page(page_num) + page = self.get_first_notebook_page(page[0]) + + + def resizeterm (self, widget, keyname): + vertical = False + if keyname in ('Up', 'Down'): + vertical = True + elif keyname in ('Left', 'Right'): + vertical = False + else: + return + parent = self.get_first_parent_paned(widget,vertical) + if parent == None: + return + + #We have a corresponding parent pane + # + #allocation = parent.get_allocation() + + if keyname in ('Up', 'Down'): + maxi = parent.get_child1().get_allocation().height + parent.get_child2().get_allocation().height - 1 + + else: + maxi = parent.get_child1().get_allocation().width + parent.get_child2().get_allocation().width - 1 + move = 10 + if keyname in ('Up', 'Left'): + move = -10 + + move = max(2, parent.get_position() + move) + move = min(maxi, move) + + parent.set_position(move) + + def previous_tab(self, term): + notebook = self.get_first_parent_notebook(term) + notebook.prev_page() + return + + def next_tab(self, term): + notebook = self.get_first_parent_notebook(term) + notebook.next_page() + return + + def move_tab(self, term, direction): + dbg("moving to direction %s" % direction) + (notebook, page) = self.get_first_notebook_page(term) + page_num = notebook.page_num(page) + nbpages = notebook.get_n_pages() + #dbg ("%s %s %s %s" % (page_num, nbpages,notebook, page)) + if page_num == 0 and direction == 'left': + new_page_num = nbpages + elif page_num == nbpages - 1 and direction == 'right': + new_page_num = 0 + elif direction == 'left': + new_page_num = page_num - 1 + elif direction == 'right': + new_page_num = page_num + 1 + else: + dbg("[ERROR] unhandled combination in move_tab: direction = %s page_num = %d" % (direction, page_num)) + return False + notebook.reorder_child(page, new_page_num) + return True + + def get_first_parent_notebook(self, widget): + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Notebook): + return parent + return self.get_first_parent_notebook(parent) + + def get_first_parent_paned (self, widget, vertical = None): + """This method returns the first parent pane of a widget. + if vertical is True returns the first VPaned + if vertical is False return the first Hpaned + if is None return the First Paned""" + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Paned) and vertical is None: + return parent + if isinstance (parent, gtk.VPaned) and vertical: + return parent + elif isinstance (parent, gtk.HPaned) and not vertical: + return parent + return self.get_first_parent_paned(parent, vertical) + + def get_first_notebook_page(self, widget): + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Notebook): + page = -1 + for i in range(0, parent.get_n_pages()): + if parent.get_nth_page(i) == widget: + return (parent, widget) + return self.get_first_notebook_page(parent) + + def reconfigure_vtes (self): + for term in self.term_list: + term.reconfigure_vte () + + def toggle_zoom(self, widget, fontscale = False): + if not self._zoomed: + widget._titlebars = widget._titlebox.get_property ('visible') + dbg ('toggle_zoom: not zoomed. remembered titlebar setting of %s'%widget._titlebars) + if widget._titlebars: + widget._titlebox.hide() + self.zoom_term (widget, fontscale) + else: + dbg ('toggle_zoom: zoomed. restoring titlebar setting of %s'%widget._titlebars) + self.unzoom_term (widget, True) + if widget._titlebars and \ + len(self.term_list) > 1 \ + and \ + (isinstance(widget, TerminatorTerm) and isinstance(widget.get_parent(),gtk.Paned))\ + : + widget._titlebox.show() + widget._vte.grab_focus() + + def zoom_term (self, widget, fontscale = False): + """Maximize to full window an instance of TerminatorTerm.""" + self.old_font = widget._vte.get_font () + self.old_columns = widget._vte.get_column_count () + self.old_rows = widget._vte.get_row_count () + self.old_parent = widget.get_parent() + + if isinstance(self.old_parent, gtk.Window): + return + if isinstance(self.old_parent, gtk.Notebook): + self.old_page = self.old_parent.get_current_page() + + self.window_child = self.window.get_children()[0] + self.window.remove(self.window_child) + self.old_parent.remove(widget) + self.window.add(widget) + self._zoomed = True + + if fontscale: + self.cnid = widget.connect ("size-allocate", self.zoom_scale_font) + else: + self._maximised = True + + widget._vte.grab_focus () + + def zoom_scale_font (self, widget, allocation): + new_columns = widget._vte.get_column_count () + new_rows = widget._vte.get_row_count () + new_font = widget._vte.get_font () + + dbg ('zoom_scale_font: I just went from %dx%d to %dx%d. Raa!'%(self.old_columns, self.old_rows, new_columns, new_rows)) + + if new_rows != self.old_rows: + titleheight = widget._titlebox.get_allocation().height + vtecharheight = widget._vte.get_char_height() + rowdiff = new_rows - self.old_rows + 2 + dbg ('zoom_scale_font: titlebox height is %d, char_height is %d'%(titleheight, vtecharheight)) + dbg ('zoom_scale_font: lhs: %d, rhs: %f'%((titleheight / vtecharheight), rowdiff)) + care_height = (rowdiff <= vtecharheight / rowdiff) + dbg ('zoom_scale_font: caring about height difference: %s'%care_height) + else: + care_height = False + + if (new_rows <= self.old_rows) or care_height or (new_columns <= self.old_columns): + dbg ('zoom_scale_font: Which means I didnt scale on one axis (col: %s, row: %s). Bailing'%((new_columns <= self.old_columns), (new_rows <= self.old_rows))) + return + + old_area = self.old_columns * self.old_rows + new_area = new_columns * new_rows + area_factor = new_area / old_area + + dbg ('zoom_scale_font: My area changed from %d characters to %d characters, a factor of %f.'%(old_area, new_area, area_factor)) + + new_font.set_size (self.old_font.get_size() * (area_factor / 2)) + dbg ('zoom_scale_font: Scaled font from %f to %f'%(self.old_font.get_size () / pango.SCALE, new_font.get_size () / pango.SCALE)) + widget._vte.set_font (new_font) + widget.disconnect (self.cnid) + + def unzoom_term (self, widget, fontscale = False): + """Proof of concept: Go back to previous application + widget structure. + """ + if self._zoomed: + if fontscale: + widget._vte.set_font (self.old_font) + self._zoomed = False + self._maximised = False + + self.window.remove(widget) + self.window.add(self.window_child) + self.old_parent.add(widget) + if isinstance(self.old_parent, gtk.Notebook): + self.old_parent.set_current_page(self.old_page) + + widget._vte.grab_focus () + diff --git a/terminatorlib/terminatorterm.py b/terminatorlib/terminatorterm.py new file mode 100755 index 00000000..48ad1ad9 --- /dev/null +++ b/terminatorlib/terminatorterm.py @@ -0,0 +1,1729 @@ +#!/usr/bin/python +# Terminator - multiple gnome terminals in one window +# Copyright (C) 2006-2008 cmsj@tenshu.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 2 only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Terminator by Chris Jones """ +import gobject, gtk, pango +import os, platform, sys + +#import version details +from terminatorlib.version import * + +# import our configuration loader +from terminatorlib import config +from terminatorlib.config import dbg, err + +#import encoding list +from terminatorlib.encoding import TerminatorEncoding + +# import vte-bindings +try: + import vte +except: + error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, + _('You need to install python bindings for libvte ("python-vte" in debian/ubuntu)')) + error.run() + sys.exit (1) + +TARGET_TYPE_VTE = 8 + +# Sort out cwd detection code, if available +pid_get_cwd = lambda pid: None +if platform.system() == 'FreeBSD': + try: + from terminatorlib import freebsd + pid_get_cwd = lambda pid: freebsd.get_process_cwd(pid) + dbg ('Using FreeBSD pid_get_cwd') + except: + dbg ('FreeBSD version too old for pid_get_cwd') + pass +elif platform.system() == 'Linux': + dbg ('Using Linux pid_get_cwd') + pid_get_cwd = lambda pid: os.path.realpath ('/proc/%s/cwd' % pid) +else: + dbg ('Unable to set a pid_get_cwd, unknown system: %s'%platform.system) + +# import a library for viewing URLs +try: + # gnome.url_show() is really useful + import gnome + url_show = gnome.url_show +except: + # webbrowser.open() is not really useful, but will do as a fallback + import webbrowser + url_show = webbrowser.open + +def openurl (url): + try: + if subprocess.call(["xdg-open", url]) != 0: + raise + except: + try: + url_show (url) + except: + pass + +class TerminatorTerm (gtk.VBox): + + matches = {} + + def __init__ (self, terminator, profile = None, command = None, cwd = None): + gtk.VBox.__init__ (self) + self.terminator = terminator + self.conf = terminator.conf + self.command = command + + self.cwd = cwd or os.getcwd(); + if not os.path.exists(self.cwd) or not os.path.isdir(self.cwd): + self.cwd = pwd.getpwuid(os.getuid ())[5] + + self.clipboard = gtk.clipboard_get (gtk.gdk.SELECTION_CLIPBOARD) + self.scrollbar_position = self.conf.scrollbar_position + + self._vte = vte.Terminal () + self._vte.set_size (80, 24) + self.reconfigure_vte () + self._vte.show () + + self._termbox = gtk.HBox () + self._termbox.show() + self._title = gtk.Label() + self._title.show() + self._titlebox = gtk.EventBox() + self._titlebox.add(self._title) + self.show() + self.pack_start(self._titlebox, False) + self.pack_start(self._termbox) + + if self.conf.titlebars: + self._titlebox.show() + else: + self._titlebox.hide() + + self._scrollbar = gtk.VScrollbar (self._vte.get_adjustment ()) + if self.scrollbar_position != "hidden" and self.scrollbar_position != "disabled": + self._scrollbar.show () + + if self.scrollbar_position == 'left': + packfunc = self._termbox.pack_end + else: + packfunc = self._termbox.pack_start + + packfunc (self._vte) + packfunc (self._scrollbar, False) + + self._vte.connect ("key-press-event", self.on_vte_key_press) + self._vte.connect ("button-press-event", self.on_vte_button_press) + self._vte.connect ("popup-menu", self.on_vte_popup_menu) + + """drag and drop""" + srcvtetargets = [ ( "vte", gtk.TARGET_SAME_APP, TARGET_TYPE_VTE ) ] + dsttargets = [ ( "vte", gtk.TARGET_SAME_APP, TARGET_TYPE_VTE ), ('text/plain', 0, 0) , ("STRING", 0, 0), ("COMPOUND_TEXT", 0, 0)] + self._vte.drag_source_set( gtk.gdk.CONTROL_MASK | gtk.gdk.BUTTON3_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) + self._titlebox.drag_source_set( gtk.gdk.BUTTON1_MASK, srcvtetargets, gtk.gdk.ACTION_MOVE) + #self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) + self._vte.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT |gtk.DEST_DEFAULT_DROP ,dsttargets, gtk.gdk.ACTION_MOVE) + self._vte.connect("drag-begin", self.on_drag_begin, self) + self._titlebox.connect("drag-begin", self.on_drag_begin, self) + self._vte.connect("drag-data-get", self.on_drag_data_get, self) + self._titlebox.connect("drag-data-get", self.on_drag_data_get, self) + #for testing purpose: drag-motion + self._vte.connect("drag-motion", self.on_drag_motion, self) + self._vte.connect("drag-data-received", self.on_drag_data_received, self) + + if self.conf.copy_on_selection: + self._vte.connect ("selection-changed", lambda widget: self._vte.copy_clipboard ()) + + self._vte.connect ("composited-changed", self.on_composited_changed) + self._vte.connect ("window-title-changed", self.on_vte_title_change) + self._vte.connect ("grab-focus", self.on_vte_focus) + self._vte.connect ("focus-out-event", self.on_vte_focus_out) + self._vte.connect ("focus-in-event", self.on_vte_focus_in) + + + exit_action = self.conf.exit_action + if exit_action == "restart": + self._vte.connect ("child-exited", self.spawn_child) + # We need to support "left" because some buggy versions of gnome-terminal + # set it in some situations + elif exit_action in ("close", "left"): + self._vte.connect ("child-exited", lambda close_term: self.terminator.closeterm (self)) + + self._vte.add_events (gtk.gdk.ENTER_NOTIFY_MASK) + self._vte.connect ("enter_notify_event", self.on_vte_notify_enter) + + self.add_matches() + + dbg ('SEGBUG: Setting http_proxy') + env_proxy = os.getenv ('http_proxy') + if not env_proxy and self.conf.http_proxy and self.conf.http_proxy != '': + os.putenv ('http_proxy', self.conf.http_proxy) + + dbg ('SEGBUG: Setting COLORTERM') + os.putenv ('COLORTERM', 'gnome-terminal') + dbg ('SEGBUG: TerminatorTerm __init__ complete') + + def on_drag_begin(self, widget, drag_context, data): + dbg ('Drag begins') + widget.drag_source_set_icon_pixbuf(self.terminator.icon_theme.load_icon (APP_NAME, 48, 0)) + + def on_drag_data_get(self,widget, drag_context, selection_data, info, time, data): + dbg ("Drag data get") + selection_data.set("vte",info, str(data.terminator.term_list.index (self))) + + + def on_drag_motion(self, widget, drag_context, x, y, time, data): + dbg ("Drag Motion on ") + """ +x-special/gnome-icon-list +text/uri-list +UTF8_STRING +COMPOUND_TEXT +TEXT +STRING +text/plain;charset=utf-8 +text/plain;charset=UTF-8 +text/plain + """ + + if 'text/plain' in drag_context.targets: + #copy text from another widget + return + srcwidget = drag_context.get_source_widget() + if (isinstance(srcwidget, gtk.EventBox) and srcwidget == self._titlebox) or widget == srcwidget: + #on self + return + + + alloc = widget.allocation + rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height) + widget.window.invalidate_rect(rect, True) + widget.window.process_updates(True) + + context = widget.window.cairo_create() + if self.conf.use_theme_colors: + color = self._vte.get_style ().text[gtk.STATE_NORMAL] + else: + color = gtk.gdk.color_parse (self.conf.foreground_color) + + + context.set_source_rgba(color.red, color.green, color.blue, 0.5) + + pos = self.get_location(widget, x, y) + topleft = (0,0) + topright = (alloc.width,0) + topmiddle = (alloc.width/2,0) + bottomleft = (0, alloc.height) + bottomright = (alloc.width,alloc.height) + bottommiddle = (alloc.width/2, alloc.height) + middle = (alloc.width/2, alloc.height/2) + middleleft = (0, alloc.height/2) + middleright = (alloc.width, alloc.height/2) + #print "%f %f %d %d" %(coef1, coef2, b1,b2) + coord = () + if pos == "right": + coord = (topright, topmiddle, bottommiddle, bottomright) + if pos == "top": + coord = (topleft, topright, middleright , middleleft) + if pos == "left": + coord = (topleft, topmiddle, bottommiddle, bottomleft) + if pos == "bottom": + coord = (bottomleft, bottomright, middleright , middleleft) + + if len(coord) > 0 : + context.move_to(coord[len(coord)-1][0],coord[len(coord)-1][1]) + for i in coord: + context.line_to(i[0],i[1]) + + context.fill() + + + def on_drag_drop(self, widget, drag_context, x, y, time): + parent = widget.get_parent() + dbg ('Drag drop on %s'%parent) + + def on_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time, data): + dbg ("Drag Data Received") + if selection_data.type == 'text/plain': + #copy text to destination + #print "%s %s" % (selection_data.type, selection_data.target) + txt = selection_data.data.strip() + if txt[0:7] == "file://": + txt = "'%s'" % txt[7:] + self._vte.feed_child(txt) + return + + widgetsrc = data.terminator.term_list[int(selection_data.data)] + srcvte = drag_context.get_source_widget() + #check if computation requireds + if (isinstance(srcvte, gtk.EventBox) and srcvte == self._titlebox) or srcvte == widget: + dbg (" on itself") + return + + srchbox = widgetsrc + dsthbox = widget.get_parent().get_parent() + + dstpaned = dsthbox.get_parent() + srcpaned = srchbox.get_parent() + if isinstance(dstpaned, gtk.Window) and isinstance(srcpaned, gtk.Window): + dbg (" Only one terminal") + return + pos = self.get_location(widget, x, y) + + data.terminator.remove(widgetsrc) + data.terminator.add(self, widgetsrc,pos) + return + + + def get_location(self, vte, x, y): + pos = "" + #get the diagonales function for the receiving widget + coef1 = float(vte.allocation.height)/float(vte.allocation.width) + coef2 = -float(vte.allocation.height)/float(vte.allocation.width) + b1 = 0 + b2 = vte.allocation.height + #determine position in rectangle + """ + -------- + |\ /| + | \ / | + | \/ | + | /\ | + | / \ | + |/ \| + -------- + """ + if (x*coef1 + b1 > y ) and (x*coef2 + b2 < y ): + pos = "right" + if (x*coef1 + b1 > y ) and (x*coef2 + b2 > y ): + pos = "top" + if (x*coef1 + b1 < y ) and (x*coef2 + b2 > y ): + pos = "left" + if (x*coef1 + b1 < y ) and (x*coef2 + b2 < y ): + pos = "bottom" + return pos + + + def add_matches (self, lboundry="[[:<:]]", rboundry="[[:>:]]"): + userchars = "-A-Za-z0-9" + passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" + hostchars = "-A-Za-z0-9" + pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'" + schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" + user = "[" + userchars + "]+(:[" + passchars + "]+)?" + urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" + + self.matches['full_uri'] = self._vte.match_add(lboundry + schemes + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") + + # FreeBSD works with [[:<:]], Linux works with \< + if self.matches['full_uri'] == -1: + if lboundry != "\\<": + self.add_matches(lboundry = "\\<", rboundry = "\\>") + else: + self.matches['addr_only'] = self._vte.match_add (lboundry + "(www|ftp)[" + hostchars + "]*\.[" + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") + self.matches['email'] = self._vte.match_add (lboundry + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+[.a-zA-Z0-9-]*" + rboundry) + self.matches['nntp'] = self._vte.match_add (lboundry + '''news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@[-A-Za-z0-9.]+(:[0-9]+)?''' + rboundry) + + def spawn_child (self, event=None): + update_records = self.conf.update_records + login = self.conf.login_shell + args = [] + shell = '' + + if self.command: + dbg ('spawn_child: using self.command: %s'%self.command) + args = self.command + shell = self.command[0] + elif self.conf.use_custom_command: + dbg ('spawn_child: using custom command: %s'%self.conf.custom_command) + args = self.conf.custom_command.split () + shell = args[0] + + try: + if os.environ['PATH'] == "": + raise (ValueError) + paths = os.environ['PATH'].split(':') + except: + paths = ['/usr/local/bin', '/usr/bin', '/bin'] + dbg ('spawn_child: found paths: "%s"'%paths) + + if self.conf.use_custom_command and shell[0] != '/': + for path in paths: + dbg ('spawn_child: looking for pathless custom command "%s"'%os.path.join (path, shell)) + if os.path.exists (os.path.join (path, shell)): + shell = os.path.join (path, shell) + break + + if not self.command and not os.path.exists (shell): + dbg ('spawn_child: hunting for a command') + shell = os.getenv ('SHELL') or '' + args = [] + if not os.path.exists (shell): + dbg ('spawn_child: No usable shell in $SHELL (%s)'%os.getenv('SHELL')) + shell = pwd.getpwuid (os.getuid ())[6] or '' + if not os.path.exists (shell): + for i in ['bash','zsh','tcsh','ksh','csh','sh']: + for p in paths: + shell = os.path.join(p, i) + dbg ('spawn_child: Checking if "%s" exists'%shell) + if not os.path.exists (shell): + dbg ('spawn_child: %s does not exist'%shell) + continue + else: + dbg ('spawn_child: %s does exist'%shell) + break + if os.path.exists (shell): + break + + if not self.command and not os.path.exists (shell): + # Give up, we're completely stuck + err (_('Unable to find a shell')) + gobject.timeout_add (100, self.terminator.closeterm, self) + return (-1) + + if not args: + args.append (shell) + + if self.conf.login_shell: + args[0] = "-%s"%args[0] + + dbg ('SEGBUG: Setting WINDOWID') + os.putenv ('WINDOWID', '%s'%self._vte.get_parent_window().xid) + + dbg ('SEGBUG: Forking command: "%s" with args "%s", loglastlog = "%s", logwtmp = "%s", logutmp = "%s" and cwd "%s"'%(shell, args, login, update_records, update_records, self.cwd)) + self._pid = self._vte.fork_command (command = shell, argv = args, envv = [], loglastlog = login, logwtmp = update_records, logutmp = update_records, directory=self.cwd) + + dbg ('SEGBUG: Forked command') + if self._pid == -1: + err (_('Unable to start shell: ') + shell) + return (-1) + + def get_cwd (self): + """ Return the current working directory of the subprocess. + This function requires OS specific behaviours + """ + cwd = pid_get_cwd (self._pid) + dbg ('get_cwd found: %s'%cwd) + return (cwd) + + def reconfigure_vte (self): + # Set our emulation + self._vte.set_emulation (self.conf.emulation) + + # Set our wordchars + self._vte.set_word_chars (self.conf.word_chars) + + # Set our mouselation + self._vte.set_mouse_autohide (self.conf.mouse_autohide) + + # Set our compatibility + backspace = self.conf.backspace_binding + delete = self.conf.delete_binding + +# Note, each of the 4 following comments should replace the line beneath it, but the python-vte bindings don't appear to support this constant, so the magic values are being assumed from the C enum :/ + if backspace == "ascii-del": +# backbind = vte.ERASE_ASCII_BACKSPACE + backbind = 2 + else: +# backbind = vte.ERASE_AUTO_BACKSPACE + backbind = 1 + + if delete == "escape-sequence": +# delbind = vte.ERASE_DELETE_SEQUENCE + delbind = 3 + else: +# delbind = vte.ERASE_AUTO + delbind = 0 + + self._vte.set_backspace_binding (backbind) + self._vte.set_delete_binding (delbind) + + # Set our font + try: + self._vte.set_font (pango.FontDescription (self.conf.font)) + except: + pass + + # Set our boldness + self._vte.set_allow_bold (self.conf.allow_bold) + + # Set our color scheme + palette = self.conf.palette + if self.conf.use_theme_colors: + fg_color = self._vte.get_style ().text[gtk.STATE_NORMAL] + bg_color = self._vte.get_style ().base[gtk.STATE_NORMAL] + else: + fg_color = gtk.gdk.color_parse (self.conf.foreground_color) + bg_color = gtk.gdk.color_parse (self.conf.background_color) + + colors = palette.split (':') + palette = [] + for color in colors: + if color: + palette.append (gtk.gdk.color_parse (color)) + self._vte.set_colors (fg_color, bg_color, palette) + + # Set our background image, transparency and type + # Many thanks to the authors of gnome-terminal, on which this code is based. + background_type = self.conf.background_type + + # set background image settings + if background_type == "image": + self._vte.set_background_image_file (self.conf.background_image) + self._vte.set_scroll_background (self.conf.scroll_background) + else: + self._vte.set_background_image_file('') + self._vte.set_scroll_background(False) + + # set transparency for the background (image) + if background_type in ("image", "transparent"): + self._vte.set_background_tint_color (bg_color) + self._vte.set_background_saturation(1 - (self.conf.background_darkness)) + self._vte.set_opacity(int(self.conf.background_darkness * 65535)) + else: + self._vte.set_background_saturation(1) + self._vte.set_opacity(65535) + + if not self._vte.is_composited(): + self._vte.set_background_transparent (background_type == "transparent") + else: + self._vte.set_background_transparent (False) + + # Set our cursor blinkiness + self._vte.set_cursor_blinks = (self.conf.cursor_blink) + + # Set our audible belliness + silent_bell = self.conf.silent_bell + self._vte.set_audible_bell (not silent_bell) + + # Set our visual flashiness + self._vte.set_visible_bell (silent_bell) + + # Override our flashybelliness + if self.conf.force_no_bell: + self._vte.set_visible_bell (False) + self._vte.set_audible_bell (False) + + # Set our scrolliness + self._vte.set_scrollback_lines (self.conf.scrollback_lines) + self._vte.set_scroll_on_keystroke (self.conf.scroll_on_keystroke) + self._vte.set_scroll_on_output (self.conf.scroll_on_output) + + if self.scrollbar_position != self.conf.scrollbar_position: + self.scrollbar_position = self.conf.scrollbar_position + + if self.scrollbar_position == 'hidden' or self.scrollbar_position == 'disabled': + self._scrollbar.hide () + else: + self._scrollbar.show () + if self.scrollbar_position == 'right': + self._termbox.reorder_child (self._vte, 0) + elif self.scrollbar_position == 'left': + self._termbox.reorder_child (self._scrollbar, 0) + + # Set our sloppiness + self.focus = self.conf.focus + + self._vte.queue_draw () + + def on_composited_changed (self, widget): + self.reconfigure_vte () + + def on_vte_button_press (self, term, event): + # Left mouse button + Ctrl while over a link should open it + mask = gtk.gdk.CONTROL_MASK + if (event.state & mask) == mask: + if event.button == 1: + url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) + if url: + if (url[0][0:7] != "mailto:") & (url[1] == self.matches['email']): + address = "mailto:" + url[0] + else: + address = url[0] + openurl ( address ) + return False + + # Left mouse button should transfer focus to this vte widget + #LP#242612: + # we also need to give focus on the widget where the paste occured + if event.button in (1 ,2): + self._vte.grab_focus () + return False + + # Right mouse button should display a context menu if ctrl not pressed + if event.button == 3 and event.state & gtk.gdk.CONTROL_MASK == 0: + self.do_popup (event) + return True + + def on_vte_notify_enter (self, term, event): + if (self.focus == "sloppy" or self.focus == "mouse"): + term.grab_focus () + return False + + def do_scrollbar_toggle (self): + self.toggle_widget_visibility (self._scrollbar) + + def do_title_toggle (self): + self.toggle_widget_visibility (self._titlebox) + + def toggle_widget_visibility (self, widget): + if not isinstance (widget, gtk.Widget): + raise TypeError + + if widget.get_property ('visible'): + widget.hide () + else: + widget.show () + + + def paste_clipboard(self): + self._vte.paste_clipboard() + self._vte.grab_focus() + + + #keybindings for the individual splited terminals (affects only the + #the selected terminal) + def on_vte_key_press (self, term, event): + keyname = gtk.gdk.keyval_name (event.keyval) + + mask = gtk.gdk.CONTROL_MASK + if (event.state & mask) == mask: + if keyname == 'plus': + self.zoom (True) + return (True) + elif keyname == 'minus': + self.zoom (False) + return (True) + elif keyname == 'equal': + self.zoom_orig () + return (True) + + mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK + if (event.state & mask) == mask: + #Top level tab + if keyname == 'T': + self.terminator.newtab (self, True) + return (True) + # bindings that should be moved to Terminator as they all just call + # a function of Terminator. It would be cleaner is TerminatorTerm + # has absolutely no reference to Terminator. + # N (next) - P (previous) - O (horizontal) - E (vertical) - W (close) + + mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK + if (event.state & mask) == mask: + if keyname == 'N': + self.terminator.go_next (self) + return (True) + elif keyname == "P": + self.terminator.go_prev (self) + return (True) + elif keyname == 'O': + self.terminator.splitaxis (self, False) + return (True) + elif keyname == 'E': + self.terminator.splitaxis (self, True) + return (True) + elif keyname == 'W': + self.terminator.closeterm (self) + return (True) + elif keyname == 'C': + self._vte.copy_clipboard () + return (True) + elif keyname == 'V': + self.paste_clipboard () + return (True) + elif keyname == 'S': + self.do_scrollbar_toggle () + return (True) + elif keyname == 'T': + self.terminator.newtab(self) + return (True) + elif keyname in ('Up', 'Down', 'Left', 'Right'): + self.terminator.resizeterm (self, keyname) + return (True) + elif keyname == 'Page_Down': + self.terminator.move_tab(self, 'right') + return (True) + elif keyname == 'Page_Up': + self.terminator.move_tab(self, 'left') + return (True) + elif keyname == 'Z': + self.terminator.toggle_zoom (self, True) + return (True) + elif keyname == 'X': + self.terminator.toggle_zoom (self) + return (True) + + mask = gtk.gdk.CONTROL_MASK + if (event.state & mask) == mask: + if keyname == 'Page_Down': + self.terminator.next_tab(self) + return (True) + elif keyname == 'Page_Up': + self.terminator.previous_tab(self) + return (True) + + if keyname and (keyname == 'Tab' or keyname.endswith('_Tab')): + mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK + if (event.state & mask) == mask: + self.terminator.go_prev (self) + return (True) + mask = gtk.gdk.CONTROL_MASK + if (event.state & mask) == mask: + self.terminator.go_next (self) + return (True) + # Warning, mask value is either gtk.gdk.CONTROL_MASK or gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK + # if you intend to use it, reinit it + return (False) + + def zoom_orig (self): + self._vte.set_font (pango.FontDescription (self.conf.font)) + + def zoom (self, zoom_in): + pangodesc = self._vte.get_font () + fontsize = pangodesc.get_size () + + if fontsize > pango.SCALE and not zoom_in: + fontsize -= pango.SCALE + elif zoom_in: + fontsize += pango.SCALE + + pangodesc.set_size (fontsize) + self._vte.set_font (pangodesc) + + def on_vte_popup_menu (self, term, event): + self.do_popup (event) + + def do_popup (self, event = None): + menu = self.create_popup_menu (event) + menu.popup (None, None, None, event.button, event.time) + + def create_popup_menu (self, event): + menu = gtk.Menu () + url = None + + if event: + url = self._vte.match_check (int (event.x / self._vte.get_char_width ()), int (event.y / self._vte.get_char_height ())) + + if url: + if url[1] != self.matches['email']: + address = url[0] + nameopen = _("_Open Link") + namecopy = _("_Copy Link Address") + iconopen = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) + + item = gtk.ImageMenuItem (nameopen) + item.set_property('image', iconopen) + else: + if url[0][0:7] != "mailto:": + address = "mailto:" + url[0] + else: + address = url[0] + nameopen = _("_Send Mail To...") + namecopy = _("_Copy Email Address") + + item = gtk.MenuItem (nameopen) + + item.connect ("activate", lambda menu_item: openurl (address)) + menu.append (item) + + item = gtk.MenuItem (namecopy) + item.connect ("activate", lambda menu_item: self.clipboard.set_text (url[0])) + menu.append (item) + + item = gtk.MenuItem () + menu.append (item) + + item = gtk.ImageMenuItem (gtk.STOCK_COPY) + item.connect ("activate", lambda menu_item: self._vte.copy_clipboard ()) + item.set_sensitive (self._vte.get_has_selection ()) + menu.append (item) + + item = gtk.ImageMenuItem (gtk.STOCK_PASTE) + item.connect ("activate", lambda menu_item: self.paste_clipboard ()) + menu.append (item) + + item = gtk.MenuItem () + menu.append (item) + + item = gtk.CheckMenuItem (_("Show _scrollbar")) + item.set_active (self._scrollbar.get_property ('visible')) + item.connect ("toggled", lambda menu_item: self.do_scrollbar_toggle ()) + menu.append (item) + + item = gtk.CheckMenuItem (_("Show _titlebar")) + item.set_active (self._titlebox.get_property ('visible')) + item.connect ("toggled", lambda menu_item: self.do_title_toggle ()) + menu.append (item) + + self._do_encoding_items (menu) + + item = gtk.MenuItem () + menu.append (item) + + if not self.terminator._zoomed: + str_horiz = _("Split H_orizontally") + str_vert = _("Split V_ertically") + + item = gtk.ImageMenuItem (str_horiz) + item_image = gtk.Image () + item_image.set_from_icon_name (APP_NAME + '_horiz', gtk.ICON_SIZE_MENU) + item.set_image (item_image) + + item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, False)) + menu.append (item) + + item = gtk.ImageMenuItem (str_vert) + item_image = gtk.Image () + item_image.set_from_icon_name (APP_NAME + '_vert', gtk.ICON_SIZE_MENU) + item.set_image (item_image) + + item.connect ("activate", lambda menu_item: self.terminator.splitaxis (self, True)) + menu.append (item) + + item = gtk.MenuItem (_("Open _Tab")) + item.connect ("activate", lambda menu_item: self.terminator.newtab (self)) + menu.append (item) + + if self.conf.extreme_tabs: + item = gtk.MenuItem (_("Open Top Level Tab")) + item.connect ("activate", lambda menu_item: self.terminator.newtab (self, True)) + menu.append (item) + + item = gtk.MenuItem () + menu.append (item) + + if len (self.terminator.term_list) > 1: + if not self.terminator._zoomed: + item = gtk.MenuItem (_("_Zoom terminal")) + item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) + menu.append (item) + + item = gtk.MenuItem (_("_Maximise terminal")) + item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) + menu.append (item) + else: + if self.terminator._zoomed and not self.terminator._maximised: + item = gtk.MenuItem (_("_Unzoom terminal")) + item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self, True)) + menu.append (item) + + if self.terminator._zoomed and self.terminator._maximised: + item = gtk.MenuItem (_("U_nmaximise terminal")) + item.connect ("activate", lambda menu_item: self.terminator.toggle_zoom (self)) + menu.append (item) + + item = gtk.MenuItem () + menu.append (item) + + item = gtk.ImageMenuItem (gtk.STOCK_CLOSE) + item.connect ("activate", lambda menu_item: self.terminator.closeterm (self)) + menu.append (item) + + menu.show_all () + return menu + + def on_encoding_change (self, widget, encoding): + current = self._vte.get_encoding () + if current != encoding: + dbg ('Setting Encoding to: %s'%encoding) + self._vte.set_encoding (encoding) + + def _do_encoding_items (self, menu): + active_encodings = self.conf.active_encodings + item = gtk.MenuItem (_("Encodings")) + menu.append (item) + submenu = gtk.Menu () + item.set_submenu (submenu) + + current_encoding = self._vte.get_encoding () + group = None + for encoding in active_encodings: + radioitem = gtk.RadioMenuItem (group, _(encoding)) + if group is None: + group = radioitem + + if encoding == current_encoding: + radioitem.set_active (True) + + radioitem.connect ('activate', self.on_encoding_change, encoding) + submenu.append (radioitem) + + item = gtk.MenuItem (_("Other Encodings")) + submenu.append (item) + #second level + + submenu = gtk.Menu () + item.set_submenu (submenu) + encodings = TerminatorEncoding ().get_list () + encodings.sort (lambda x, y: cmp (x[2].lower (), y[2].lower ())) + group = None + + for encoding in encodings: + if encoding[1] in active_encodings: + continue + + if encoding[1] is None: + label = "%s %s"%(encoding[2], self._vte.get_encoding ()) + else: + label = "%s %s"%(encoding[2], encoding[1]) + + radioitem = gtk.RadioMenuItem (group, label) + if group is None: + group = radioitem + + if encoding[1] == current_encoding: + radioitem.set_active (True) + + radioitem.connect ('activate', self.on_encoding_change, encoding[1]) + submenu.append (radioitem) + + def on_vte_title_change(self, vte): + if self.conf.titletips: + vte.set_property ("has-tooltip", True) + vte.set_property ("tooltip-text", vte.get_window_title ()) + #set the title anyhow, titlebars setting only show/hide the label + self._title.set_text(vte.get_window_title ()) + self.terminator.set_window_title("%s: %s" %(APP_NAME.capitalize(), vte.get_window_title ())) + notebookpage = self.terminator.get_first_notebook_page(vte) + while notebookpage != None: + notebookpage[0].set_tab_label_text(notebookpage[1], vte.get_window_title ()) + notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) + + def on_vte_focus_in(self, vte, event): + self._titlebox.modify_bg(gtk.STATE_NORMAL,self.terminator.window.get_style().bg[gtk.STATE_SELECTED]) + self._title.modify_fg(gtk.STATE_NORMAL, self.terminator.window.get_style().fg[gtk.STATE_SELECTED]) + return + + def on_vte_focus_out(self, vte, event): + self._titlebox.modify_bg(gtk.STATE_NORMAL, self.terminator.window.get_style().bg[gtk.STATE_NORMAL]) + self._title.modify_fg(gtk.STATE_NORMAL, self.terminator.window.get_style().fg[gtk.STATE_NORMAL]) + return + + def on_vte_focus(self, vte): + if vte.get_window_title (): + self.terminator.set_window_title("%s: %s" %(APP_NAME.capitalize(), vte.get_window_title ())) + notebookpage = self.terminator.get_first_notebook_page(vte) + while notebookpage != None: + notebookpage[0].set_tab_label_text(notebookpage[1], vte.get_window_title ()) + notebookpage = self.terminator.get_first_notebook_page(notebookpage[0]) + + def destroy(self): + self._vte.destroy() + +class Terminator: + def __init__ (self, profile = None, command = None, fullscreen = False, maximise = False, borderless = False): + self.profile = profile + self.command = command + + self._zoomed = False + self._maximised = False + self._fullscreen = False + self._f11_modifier = False + self.term_list = [] + stores = [] + stores.append (config.TerminatorConfValuestoreRC ()) + + try: + import gconf + if self.profile: + self.profile = gconf.escape_key (self.profile, -1) + store = config.TerminatorConfValuestoreGConf (self.profile) + store.set_reconfigure_callback (self.reconfigure_vtes) + dbg ('Terminator__init__: comparing %s and %s'%(self.profile, store.profile.split ('/').pop ())) + if self.profile == store.profile.split ('/').pop (): + # If we have been given a profile, and we loaded it, we should be higher priority than RC + dbg ('Terminator__init__: placing GConf before RC') + stores.insert (0, store) + else: + stores.append (store) + except: + pass + + self.conf = config.TerminatorConfig (stores) + + self.icon_theme = gtk.IconTheme () + + if self.conf.f11_modifier: + self._f11_modifier = True + + if self.conf.handle_size in range (0,6): + gtk.rc_parse_string(""" + style "terminator-paned-style" { + GtkPaned::handle_size = %s + } + + class "GtkPaned" style "terminator-paned-style" + """ % self.conf.handle_size) + self.window = gtk.Window () + self.window.set_title (APP_NAME.capitalize()) + + try: + self.window.set_icon (self.icon_theme.load_icon (APP_NAME, 48, 0)) + except: + self.icon = self.window.render_icon (gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_BUTTON) + self.window.set_icon (self.icon) + + self.window.connect ("key-press-event", self.on_key_press) + self.window.connect ("delete_event", self.on_delete_event) + self.window.connect ("destroy", self.on_destroy_event) + self.window.connect ("window-state-event", self.on_window_state_changed) + + self.window.set_property ('allow-shrink', True) + + if fullscreen or self.conf.fullscreen: + self.fullscreen_toggle () + + if maximise or self.conf.maximise: + self.maximize () + + if borderless or self.conf.borderless: + self.window.set_decorated (False) + + # Set RGBA colormap if possible so VTE can use real alpha + # channels for transparency. + screen = self.window.get_screen() + colormap = screen.get_rgba_colormap() + if colormap: + self.window.set_colormap(colormap) + + # Start out with just one terminal + # FIXME: This should be really be decided from some kind of profile + term = (TerminatorTerm (self, self.profile, self.command)) + self.term_list = [term] + + self.window.add (term) + term._titlebox.hide() + self.window.show () + term.spawn_child () + + def maximize (self): + """ Maximize the Terminator window.""" + self.window.maximize () + + def fullscreen_toggle (self): + """ Toggle the fullscreen state of the window. If it is in + fullscreen state, it will be unfullscreened. If it is not, it + will be set to fullscreen state. + """ + if self._fullscreen: + self.window.unfullscreen () + else: + self.window.fullscreen () + + def on_window_state_changed (self, window, event): + state = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN + self._fullscreen = bool (state) + + return (False) + + def on_delete_event (self, window, event, data=None): + if len (self.term_list) == 1: + return False + + # show dialog + dialog = gtk.Dialog (_("Close?"), window, gtk.DIALOG_MODAL, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) + dialog.set_has_separator (False) + dialog.set_resizable (False) + + primairy = gtk.Label (_('Close all terminals?')) + primairy.set_use_markup (True) + primairy.set_alignment (0, 0.5) + secundairy = gtk.Label (_("This window has %s terminals open. Closing the window will also close all terminals.") % len(self.term_list)) + secundairy.set_line_wrap(True) + primairy.set_alignment (0, 0.5) + + labels = gtk.VBox () + labels.pack_start (primairy, False, False, 6) + labels.pack_start (secundairy, False, False, 6) + + image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) + image.set_alignment (0.5, 0) + + box = gtk.HBox() + box.pack_start (image, False, False, 6) + box.pack_start (labels, False, False, 6) + dialog.vbox.pack_start (box, False, False, 12) + + dialog.show_all () + result = dialog.run () + dialog.destroy () + return not (result == gtk.RESPONSE_ACCEPT) + + def on_destroy_event (self, widget, data=None): + gtk.main_quit () + + # keybindings for the whole terminal window (affects the main + # windows containing the splited terminals) + def on_key_press (self, window, event): + """ Callback for the window to determine what to do with special + keys. Currently handled key-combo's: + * F11: toggle fullscreen state of the window. + * CTRL - SHIFT - Q: close all terminals + """ + keyname = gtk.gdk.keyval_name (event.keyval) + mask = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK + + if (keyname == 'F11' and (self._f11_modifier == False or event.state & mask)): + self.fullscreen_toggle () + return (True) + + if (event.state & mask) == mask: + if keyname == 'Q': + if not self.on_delete_event (window, gtk.gdk.Event (gtk.gdk.DELETE)): + self.on_destroy_event (window, gtk.gdk.Event (gtk.gdk.DESTROY)) + + def set_window_title(self, title): + """ + Modifies Terminator window title + """ + self.window.set_title(title) + + def add(self, widget, terminal, pos = "bottom"): + """ + Add a term to another at position pos + """ + vertical = pos in ("top", "bottom") + pane = (vertical) and gtk.VPaned () or gtk.HPaned () + + # get the parent of the provided terminal + parent = widget.get_parent () + dbg ('SEGBUG: add() Got parent') + if isinstance (parent, gtk.Window): + dbg ('SEGBUG: parent is a gtk.Window') + # We have just one term + parent.remove(widget) + dbg ('SEGBUG: removed widget from window') + if pos in ("top", "left"): + dbg ('SEGBUG: pos is in top/left') + pane.pack1 (terminal, True, True) + dbg ('SEGBUG: packed terminal') + pane.pack2 (widget, True, True) + dbg ('SEGBUG: packed widget') + else: + dbg ('SEGBUG: pos is not in top/left') + pane.pack1 (widget, True, True) + dbg ('SEGBUG: packed widget') + pane.pack2 (terminal, True, True) + dbg ('SEGBUG: packed terminal') + parent.add (pane) + dbg ('SEGBUG: added pane to parent') + + position = (vertical) and parent.allocation.height \ + or parent.allocation.width + + if (isinstance (parent, gtk.Notebook) or isinstance (parent, gtk.Window)) and widget.conf.titlebars: + #not the only term in the notebook/window anymore, need to reshow the title + dbg ('SEGBUG: Showing _titlebox') + widget._titlebox.show() + + if isinstance (parent, gtk.Notebook): + dbg ('SEGBUG: Parent is a notebook') + page = -1 + + for i in range(0, parent.get_n_pages()): + if parent.get_nth_page(i) == widget: + page = i + break + widget.reparent (pane) + if pos in ("top", "left"): + pane.remove(widget) + pane.pack1 (terminal, True, True) + pane.pack2 (widget, True, True) + else: + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + #parent.remove_page(page) + pane.show() + parent.insert_page(pane, None, page) + parent.set_tab_label_text(pane, widget._vte.get_window_title()) + parent.set_tab_label_packing(pane, True, True, gtk.PACK_START) + parent.set_tab_reorderable(pane, True) + parent.set_current_page(page) + + position = (vertical) and parent.allocation.height \ + or parent.allocation.width + + if isinstance (parent, gtk.Paned): + dbg ('SEGBUG: parent is a Paned') + # We are inside a split term + position = (vertical) and widget.allocation.height \ + or widget.allocation.width + + dbg ('SEGBUG: Preparing to reparent sibling') + if (widget == parent.get_child1 ()): + widget.reparent (pane) + parent.pack1 (pane, True, True) + else: + widget.reparent (pane) + parent.pack2 (pane, True, True) + + if pos in ("top", "left"): + dbg ('SEGBUG: pos is in top/left. Removing and re-ordering') + pane.remove(widget) + pane.pack1 (terminal, True, True) + pane.pack2 (widget, True, True) + else: + dbg ('SEGBUG: pos is not in top/left. Packing') + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + + dbg ('SEGBUG: packing widget and terminal') + pane.pack1 (widget, True, True) + pane.pack2 (terminal, True, True) + dbg ('SEGBUG: packed widget and terminal') + + # show all, set position of the divider + dbg ('SEGBUG: Showing pane') + pane.show () + dbg ('SEGBUG: Showed pane') + pane.set_position (position / 2) + dbg ('SEGBUG: Set position') + terminal.show () + + # insert the term reference into the list + index = self.term_list.index (widget) + if pos in ('bottom', 'right'): + index = index + 1 + self.term_list.insert (index, terminal) + # make the new terminal grab the focus + terminal._vte.grab_focus () + + return (terminal) + + def on_page_reordered(self, notebook, child, page_num): + #page has been reordered, we need to get the + # first term and last term + dbg ("Reordered: %d"%page_num) + nbpages = notebook.get_n_pages() + if nbpages == 1: + dbg("[ERROR] only one page in on_page_reordered") + + first = self._notebook_first_term(notebook.get_nth_page(page_num)) + last = self._notebook_last_term(notebook.get_nth_page(page_num)) + firstidx = self.term_list.index(first) + lastidx = self.term_list.index(last) + termslice = self.term_list[firstidx:lastidx+1] + #remove them from the list + for term in termslice: + self.term_list.remove(term) + + if page_num == 0: + #first page, we insert before the first term of next page + nexttab = notebook.get_nth_page(1) + sibling = self._notebook_first_term(nexttab) + siblingindex = self.term_list.index(sibling) + for term in termslice: + self.term_list.insert(siblingindex, term) + siblingindex += 1 + else: + #other pages, we insert after the last term of previous page + previoustab = notebook.get_nth_page(page_num - 1) + sibling = self._notebook_last_term(previoustab) + siblingindex = self.term_list.index(sibling) + for term in termslice: + siblingindex += 1 + self.term_list.insert(siblingindex, term) + + #for page reorder, we need to get the first term of a notebook + def notebook_first_term(self, notebook): + return self._notebook_first_term(notebook.get_nth_page(0)) + + def _notebook_first_term(self, child): + if isinstance(child, TerminatorTerm): + return child + elif isinstance(child, gtk.Paned): + return self._notebook_first_term(child.get_child1()) + elif isinstance(child, gtk.Notebook): + return self._notebook_first_term(child.get_nth_page(0)) + + dbg("[ERROR] unsupported class %s in _notebook_first_term" % child.__class__.__name__) + return None + + #for page reorder, we need to get the last term of a notebook + def notebook_last_term(self, notebook): + return self._notebook_last_term(notebook.get_nth_page(notebook.get_n_pages()-1)) + + def _notebook_last_term(self, child): + if isinstance(child, TerminatorTerm): + return child + elif isinstance(child, gtk.Paned): + return self._notebook_last_term(child.get_child2()) + elif isinstance(child, gtk.Notebook): + return self._notebook_last_term(child.get_nth_page(child.get_n_pages()-1)) + + dbg("[ERROR] unsupported class %s in _notebook_last_term" % child.__class__.__name__) + return None + + def newtab(self,widget, toplevel = False): + if self._zoomed: + # We don't want to add a new tab while we are zoomed in on a terminal + dbg ("newtab function called, but Terminator was in zoomed terminal mode.") + return + + terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) + #only one term, we don't show the title + terminal._titlebox.hide() + if self.conf.extreme_tabs and not toplevel: + parent = widget.get_parent () + child = widget + else: + child = self.window.get_children()[0] + parent = child.get_parent() + + if isinstance(parent, gtk.Paned) or (isinstance(parent, gtk.Window) + and + ((self.conf.extreme_tabs and not toplevel) or not isinstance(child, gtk.Notebook))): + #no notebook yet. + notebook = gtk.Notebook() + notebook.set_tab_pos(gtk.POS_TOP) + notebook.connect('page-reordered',self.on_page_reordered) + notebook.set_property('homogeneous', True) + notebook.set_tab_reorderable(widget, True) + + if isinstance(parent, gtk.Paned): + if parent.get_child1() == child: + child.reparent(notebook) + parent.pack1(notebook) + else: + child.reparent(notebook) + parent.pack2(notebook) + elif isinstance(parent, gtk.Window): + child.reparent(notebook) + parent.add(notebook) + notebook.set_tab_reorderable(child,True) + notebooklabel = "" + if isinstance(child, TerminatorTerm): + child._titlebox.hide() + if widget._vte.get_window_title() is not None: + notebooklabel = widget._vte.get_window_title() + notebook.set_tab_label_text(child, notebooklabel) + notebook. set_tab_label_packing(child, True, True, gtk.PACK_START) + notebook.show() + elif isinstance(parent, gtk.Notebook): + notebook = parent + elif isinstance(parent, gtk.Window) and isinstance(child, gtk.Notebook): + notebook = child + else: + return (False) + + ## NOTE + ## Here we need to append to the notebook before we can + ## spawn the terminal (WINDOW_ID needs to be set) + + notebook.append_page(terminal,None) + terminal.show () + terminal.spawn_child () + ## Some gtk/vte weirdness + ## If we don't use this silly test, + ## terminal._vte.get_window_title() might return + ## bogus values + notebooklabel = "" + if terminal._vte.get_window_title() is not None: + notebooklabel = terminal._vte.get_window_title() + notebook.set_tab_label_text(terminal, notebooklabel) + notebook.set_tab_label_packing(terminal, True, True, gtk.PACK_START) + notebook.set_tab_reorderable(terminal,True) + ## Now, we set focus on the new term + notebook.set_current_page(-1) + terminal._vte.grab_focus () + + #adding a new tab, thus we need to get the + # last term of the previous tab and add + # the new term just after + sibling = self._notebook_last_term(notebook.get_nth_page(notebook.page_num(terminal)-1)) + index = self.term_list.index(sibling) + self.term_list.insert (index + 1, terminal) + return (True) + + + return terminal + + def splitaxis (self, widget, vertical=True): + """ Split the provided widget on the horizontal or vertical axis. """ + if self._zoomed: + # We don't want to split the terminal while we are in zoomed mode + dbg ("splitaxis function called, but Terminator was in zoomed mode.") + return + + # create a new terminal and parent pane. + dbg ('SEGBUG: Creating TerminatorTerm') + terminal = TerminatorTerm (self, self.profile, None, widget.get_cwd()) + dbg ('SEGBUG: Created TerminatorTerm') + pos = vertical and "right" or "bottom" + dbg ('SEGBUG: Position is: %s'%pos) + self.add(widget, terminal, pos) + dbg ('SEGBUG: added TerminatorTerm to container') + terminal.show () + dbg ('SEGBUG: showed TerminatorTerm') + terminal.spawn_child () + dbg ('SEGBUG: spawned child') + return terminal + + def remove(self, widget): + """Remove a TerminatorTerm from the Terminator view and terms list + Returns True on success, False on failure""" + parent = widget.get_parent () + sibling = None + focus_on_close = 'prev' + if isinstance (parent, gtk.Window): + # We are the only term + if not self.on_delete_event (parent, gtk.gdk.Event (gtk.gdk.DELETE)): + self.on_destroy_event (parent, gtk.gdk.Event (gtk.gdk.DESTROY)) + return + + if isinstance (parent, gtk.Paned): + index = self.term_list.index (widget) + grandparent = parent.get_parent () + + # Discover sibling while all objects exist + if widget == parent.get_child1 (): + sibling = parent.get_child2 () + focus_on_close = 'next' + if widget == parent.get_child2 (): + sibling = parent.get_child1 () + + if not sibling: + # something is wrong, give up + err ("Error: %s is not a child of %s"%(widget, parent)) + return False + + parent.remove(widget) + if isinstance(grandparent, gtk.Notebook): + page = -1 + for i in range(0, grandparent.get_n_pages()): + if grandparent.get_nth_page(i) == parent: + page = i + break + parent.remove(sibling) + grandparent.remove_page(page) + grandparent.insert_page(sibling, None,page) + grandparent.set_tab_label_packing(sibling, True, True, gtk.PACK_START) + grandparent.set_tab_reorderable(sibling, True) + grandparent.set_current_page(page) + + else: + grandparent.remove (parent) + sibling.reparent (grandparent) + if not self._zoomed: + grandparent.resize_children() + parent.destroy () + if isinstance(sibling, TerminatorTerm) and isinstance(sibling.get_parent(), gtk.Notebook): + sibling._titlebox.hide() + + self.term_list.remove (widget) + + elif isinstance (parent, gtk.Notebook): + parent.remove(widget) + nbpages = parent.get_n_pages() + index = self.term_list.index (widget) + self.term_list.remove (widget) + if nbpages == 1: + sibling = parent.get_nth_page(0) + parent.remove(sibling) + gdparent = parent.get_parent() + if isinstance(gdparent, gtk.Window): + gdparent.remove(parent) + gdparent.add(sibling) + elif isinstance(gdparent, gtk.Paned): + if gdparent.get_child1() == parent: + gdparent.remove(parent) + gdparent.pack1(sibling) + else: + gdparent.remove(parent) + gdparent.pack2(sibling) + if isinstance(sibling, TerminatorTerm) and sibling.conf.titlebars and sibling.conf.extreme_tabs: + sibling._titlebox.show() + parent.destroy() + if self.conf.focus_on_close == 'prev' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'prev'): + if index == 0: index = 1 + self.term_list[index - 1]._vte.grab_focus () + self._set_current_notebook_page_recursive(self.term_list[index - 1]) + elif self.conf.focus_on_close == 'next' or ( self.conf.focus_on_close == 'auto' and focus_on_close == 'next'): + if index == len(self.term_list): index = index - 1 + self.term_list[index]._vte.grab_focus () + self._set_current_notebook_page_recursive(self.term_list[index]) + + if len(self.term_list) == 1: + self.term_list[0]._titlebox.hide() + + return True + + def closeterm (self, widget): + if self._zoomed: + # We are zoomed, pop back out to normal layout before closing + dbg ("closeterm function called while in zoomed mode. Restoring previous layout before closing.") + self.toggle_zoom(widget, not self._maximised) + + if self.remove(widget): + widget.destroy () + return True + return False + + + def go_next (self, term): + current = self.term_list.index (term) + next = None + if self.conf.cycle_term_tab: + notebookpage = self.get_first_notebook_page(term) + if notebookpage: + last = self._notebook_last_term(notebookpage[1]) + first = self._notebook_first_term(notebookpage[1]) + if term == last: + next = self.term_list.index(first) + + if next is None: + if current == len (self.term_list) - 1: + next = 0 + else: + next = current + 1 + + + nextterm = self.term_list[next] + ##we need to set the current page of each notebook + self._set_current_notebook_page_recursive(nextterm) + + nextterm._vte.grab_focus () + + + def go_prev (self, term): + current = self.term_list.index (term) + previous = None + + if self.conf.cycle_term_tab: + notebookpage = self.get_first_notebook_page(term) + if notebookpage: + last = self._notebook_last_term(notebookpage[1]) + first = self._notebook_first_term(notebookpage[1]) + if term == first: + previous = self.term_list.index(last) + + if previous is None: + if current == 0: + previous = len (self.term_list) - 1 + else: + previous = current - 1 + + #self.window.set_title(self.term_list[previous]._vte.get_window_title()) + previousterm = self.term_list[previous] + ##we need to set the current page of each notebook + self._set_current_notebook_page_recursive(previousterm) + previousterm._vte.grab_focus () + + + def _set_current_notebook_page_recursive(self, widget): + page = self.get_first_notebook_page(widget) + while page: + child = None + page_num = page[0].page_num(page[1]) + page[0].set_current_page(page_num) + page = self.get_first_notebook_page(page[0]) + + + def resizeterm (self, widget, keyname): + vertical = False + if keyname in ('Up', 'Down'): + vertical = True + elif keyname in ('Left', 'Right'): + vertical = False + else: + return + parent = self.get_first_parent_paned(widget,vertical) + if parent == None: + return + + #We have a corresponding parent pane + # + #allocation = parent.get_allocation() + + if keyname in ('Up', 'Down'): + maxi = parent.get_child1().get_allocation().height + parent.get_child2().get_allocation().height - 1 + + else: + maxi = parent.get_child1().get_allocation().width + parent.get_child2().get_allocation().width - 1 + move = 10 + if keyname in ('Up', 'Left'): + move = -10 + + move = max(2, parent.get_position() + move) + move = min(maxi, move) + + parent.set_position(move) + + def previous_tab(self, term): + notebook = self.get_first_parent_notebook(term) + notebook.prev_page() + return + + def next_tab(self, term): + notebook = self.get_first_parent_notebook(term) + notebook.next_page() + return + + def move_tab(self, term, direction): + dbg("moving to direction %s" % direction) + (notebook, page) = self.get_first_notebook_page(term) + page_num = notebook.page_num(page) + nbpages = notebook.get_n_pages() + #dbg ("%s %s %s %s" % (page_num, nbpages,notebook, page)) + if page_num == 0 and direction == 'left': + new_page_num = nbpages + elif page_num == nbpages - 1 and direction == 'right': + new_page_num = 0 + elif direction == 'left': + new_page_num = page_num - 1 + elif direction == 'right': + new_page_num = page_num + 1 + else: + dbg("[ERROR] unhandled combination in move_tab: direction = %s page_num = %d" % (direction, page_num)) + return False + notebook.reorder_child(page, new_page_num) + return True + + def get_first_parent_notebook(self, widget): + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Notebook): + return parent + return self.get_first_parent_notebook(parent) + + def get_first_parent_paned (self, widget, vertical = None): + """This method returns the first parent pane of a widget. + if vertical is True returns the first VPaned + if vertical is False return the first Hpaned + if is None return the First Paned""" + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Paned) and vertical is None: + return parent + if isinstance (parent, gtk.VPaned) and vertical: + return parent + elif isinstance (parent, gtk.HPaned) and not vertical: + return parent + return self.get_first_parent_paned(parent, vertical) + + def get_first_notebook_page(self, widget): + if isinstance (widget, gtk.Window): + return None + parent = widget.get_parent() + if isinstance (parent, gtk.Notebook): + page = -1 + for i in range(0, parent.get_n_pages()): + if parent.get_nth_page(i) == widget: + return (parent, widget) + return self.get_first_notebook_page(parent) + + def reconfigure_vtes (self): + for term in self.term_list: + term.reconfigure_vte () + + def toggle_zoom(self, widget, fontscale = False): + if not self._zoomed: + widget._titlebars = widget._titlebox.get_property ('visible') + dbg ('toggle_zoom: not zoomed. remembered titlebar setting of %s'%widget._titlebars) + if widget._titlebars: + widget._titlebox.hide() + self.zoom_term (widget, fontscale) + else: + dbg ('toggle_zoom: zoomed. restoring titlebar setting of %s'%widget._titlebars) + self.unzoom_term (widget, True) + if widget._titlebars and \ + len(self.term_list) > 1 \ + and \ + (isinstance(widget, TerminatorTerm) and isinstance(widget.get_parent(),gtk.Paned))\ + : + widget._titlebox.show() + widget._vte.grab_focus() + + def zoom_term (self, widget, fontscale = False): + """Maximize to full window an instance of TerminatorTerm.""" + self.old_font = widget._vte.get_font () + self.old_columns = widget._vte.get_column_count () + self.old_rows = widget._vte.get_row_count () + self.old_parent = widget.get_parent() + + if isinstance(self.old_parent, gtk.Window): + return + if isinstance(self.old_parent, gtk.Notebook): + self.old_page = self.old_parent.get_current_page() + + self.window_child = self.window.get_children()[0] + self.window.remove(self.window_child) + self.old_parent.remove(widget) + self.window.add(widget) + self._zoomed = True + + if fontscale: + self.cnid = widget.connect ("size-allocate", self.zoom_scale_font) + else: + self._maximised = True + + widget._vte.grab_focus () + + def zoom_scale_font (self, widget, allocation): + new_columns = widget._vte.get_column_count () + new_rows = widget._vte.get_row_count () + new_font = widget._vte.get_font () + + dbg ('zoom_scale_font: I just went from %dx%d to %dx%d. Raa!'%(self.old_columns, self.old_rows, new_columns, new_rows)) + + if new_rows != self.old_rows: + titleheight = widget._titlebox.get_allocation().height + vtecharheight = widget._vte.get_char_height() + rowdiff = new_rows - self.old_rows + 2 + dbg ('zoom_scale_font: titlebox height is %d, char_height is %d'%(titleheight, vtecharheight)) + dbg ('zoom_scale_font: lhs: %d, rhs: %f'%((titleheight / vtecharheight), rowdiff)) + care_height = (rowdiff <= vtecharheight / rowdiff) + dbg ('zoom_scale_font: caring about height difference: %s'%care_height) + else: + care_height = False + + if (new_rows <= self.old_rows) or care_height or (new_columns <= self.old_columns): + dbg ('zoom_scale_font: Which means I didnt scale on one axis (col: %s, row: %s). Bailing'%((new_columns <= self.old_columns), (new_rows <= self.old_rows))) + return + + old_area = self.old_columns * self.old_rows + new_area = new_columns * new_rows + area_factor = new_area / old_area + + dbg ('zoom_scale_font: My area changed from %d characters to %d characters, a factor of %f.'%(old_area, new_area, area_factor)) + + new_font.set_size (self.old_font.get_size() * (area_factor / 2)) + dbg ('zoom_scale_font: Scaled font from %f to %f'%(self.old_font.get_size () / pango.SCALE, new_font.get_size () / pango.SCALE)) + widget._vte.set_font (new_font) + widget.disconnect (self.cnid) + + def unzoom_term (self, widget, fontscale = False): + """Proof of concept: Go back to previous application + widget structure. + """ + if self._zoomed: + if fontscale: + widget._vte.set_font (self.old_font) + self._zoomed = False + self._maximised = False + + self.window.remove(widget) + self.window.add(self.window_child) + self.old_parent.add(widget) + if isinstance(self.old_parent, gtk.Notebook): + self.old_parent.set_current_page(self.old_page) + + widget._vte.grab_focus () +