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