1# -*- coding: utf-8; -*-
2"""
3Copyright (C) 2007-2012 Lincoln de Sousa <lincoln@minaslivre.org>
4Copyright (C) 2007 Gabriel Falcão <gabrielteratos@gmail.com>
5
6This program is free software; you can redistribute it and/or
7modify it under the terms of the GNU General Public License as
8published by the Free Software Foundation; either version 2 of the
9License, or (at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14General Public License for more details.
15
16You should have received a copy of the GNU General Public
17License along with this program; if not, write to the
18Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19Boston, MA 02110-1301 USA
20"""
21import json
22import logging
23import os
24import platform
25import subprocess
26import sys
27import traceback
28import uuid
29
30from pathlib import Path
31from urllib.parse import quote_plus
32from xml.sax.saxutils import escape as xml_escape
33
34import gi
35gi.require_version('Gtk', '3.0')
36gi.require_version('Gdk', '3.0')
37gi.require_version('Vte', '2.91')  # vte-0.42
38gi.require_version('Keybinder', '3.0')
39from gi.repository import GLib
40from gi.repository import GObject
41from gi.repository import Gdk
42from gi.repository import GdkX11
43from gi.repository import Gio
44from gi.repository import Gtk
45from gi.repository import Keybinder
46from gi.repository import Vte
47
48import cairo
49
50from guake import gtk_version
51from guake import guake_version
52from guake import notifier
53from guake import vte_version
54from guake.about import AboutDialog
55from guake.common import gladefile
56from guake.common import pixmapfile
57from guake.dialogs import PromptQuitDialog
58from guake.globals import ALIGN_BOTTOM
59from guake.globals import ALIGN_CENTER
60from guake.globals import ALIGN_LEFT
61from guake.globals import ALIGN_RIGHT
62from guake.globals import ALIGN_TOP
63from guake.globals import ALWAYS_ON_PRIMARY
64from guake.globals import NAME
65from guake.gsettings import GSettingHandler
66from guake.guake_logging import setupLogging
67from guake.keybindings import Keybindings
68from guake.notebook import TerminalNotebook
69from guake.paths import LOCALE_DIR
70from guake.paths import SCHEMA_DIR
71from guake.paths import try_to_compile_glib_schemas
72from guake.prefs import PrefsDialog
73from guake.prefs import refresh_user_start
74from guake.settings import Settings
75from guake.simplegladeapp import SimpleGladeApp
76from guake.terminal import GuakeTerminal
77from guake.theme import patch_gtk_theme
78from guake.theme import select_gtk_theme
79from guake.utils import FullscreenManager
80from guake.utils import HidePrevention
81from guake.utils import RectCalculator
82from guake.utils import TabNameUtils
83from guake.utils import get_server_time
84
85from gettext import gettext as _
86
87log = logging.getLogger(__name__)
88
89instance = None
90RESPONSE_FORWARD = 0
91RESPONSE_BACKWARD = 1
92
93# Disable find feature until python-vte hasn't been updated
94enable_find = False
95
96GObject.threads_init()
97
98# Setting gobject program name
99GObject.set_prgname(NAME)
100
101GDK_WINDOW_STATE_WITHDRAWN = 1
102GDK_WINDOW_STATE_ICONIFIED = 2
103GDK_WINDOW_STATE_STICKY = 8
104GDK_WINDOW_STATE_ABOVE = 32
105
106# Transparency max level (should be always 100)
107MAX_TRANSPARENCY = 100
108
109
110class Guake(SimpleGladeApp):
111
112    """Guake main class. Handles specialy the main window.
113    """
114
115    def __init__(self):
116
117        def load_schema():
118            return Gio.SettingsSchemaSource.new_from_directory(
119                SCHEMA_DIR, Gio.SettingsSchemaSource.get_default(), False
120            )
121
122        try:
123            schema_source = load_schema()
124        except GLib.Error:  # pylint: disable=catching-non-exception
125            log.exception("Unable to load the GLib schema, try to compile it")
126            try_to_compile_glib_schemas()
127            schema_source = load_schema()
128        self.settings = Settings(schema_source)
129
130        super(Guake, self).__init__(gladefile('guake.glade'))
131
132        select_gtk_theme(self.settings)
133        patch_gtk_theme(self.get_widget("window-root").get_style_context(), self.settings)
134        self.add_callbacks(self)
135
136        self.debug_mode = self.settings.general.get_boolean('debug-mode')
137        setupLogging(self.debug_mode)
138        log.info('Guake Terminal %s', guake_version())
139        log.info('VTE %s', vte_version())
140        log.info('Gtk %s', gtk_version())
141
142        self.hidden = True
143        self.forceHide = False
144
145        # trayicon! Using SVG handles better different OS trays
146        # img = pixmapfile('guake-tray.svg')
147        # trayicon!
148        img = pixmapfile('guake-tray.png')
149        try:
150            import appindicator
151        except ImportError:
152            self.tray_icon = Gtk.StatusIcon()
153            self.tray_icon.set_from_file(img)
154            self.tray_icon.set_tooltip_text(_("Guake Terminal"))
155            self.tray_icon.connect('popup-menu', self.show_menu)
156            self.tray_icon.connect('activate', self.show_hide)
157        else:
158            # TODO PORT test this on a system with app indicator
159            self.tray_icon = appindicator.Indicator(
160                _("guake-indicator"), _("guake-tray"), appindicator.CATEGORY_OTHER
161            )
162            self.tray_icon.set_icon(img)
163            self.tray_icon.set_status(appindicator.STATUS_ACTIVE)
164            menu = self.get_widget('tray-menu')
165            show = Gtk.MenuItem(_('Show'))
166            show.set_sensitive(True)
167            show.connect('activate', self.show_hide)
168            show.show()
169            menu.prepend(show)
170            self.tray_icon.set_menu(menu)
171
172        # important widgets
173        self.window = self.get_widget('window-root')
174        self.window.set_keep_above(True)
175        self.mainframe = self.get_widget('mainframe')
176        self.mainframe.remove(self.get_widget('notebook-teminals'))
177        self.notebook = TerminalNotebook(self)
178        self.notebook.connect('terminal-spawned', self.terminal_spawned)
179        self.notebook.connect('page-deleted', self.page_deleted)
180
181        self.mainframe.add(self.notebook)
182        self.set_tab_position()
183
184        # check and set ARGB for real transparency
185        color = self.window.get_style_context().get_background_color(Gtk.StateFlags.NORMAL)
186        self.window.set_app_paintable(True)
187
188        def draw_callback(widget, cr):
189            if widget.transparency:
190                cr.set_source_rgba(color.red, color.green, color.blue, 1)
191            else:
192                cr.set_source_rgb(0, 0, 0)
193            cr.set_operator(cairo.OPERATOR_SOURCE)
194            cr.paint()
195            cr.set_operator(cairo.OPERATOR_OVER)
196
197        screen = self.window.get_screen()
198        visual = screen.get_rgba_visual()
199
200        if visual and screen.is_composited():
201            self.window.set_visual(visual)
202            self.window.transparency = True
203        else:
204            log.warn('System doesn\'t support transparency')
205            self.window.transparency = False
206            self.window.set_visual(screen.get_system_visual())
207
208        self.window.connect('draw', draw_callback)
209
210        # holds the timestamp of the losefocus event
211        self.losefocus_time = 0
212
213        # holds the timestamp of the previous show/hide action
214        self.prev_showhide_time = 0
215
216        # Controls the transparency state needed for function accel_toggle_transparency
217        self.transparency_toggled = False
218
219        # store the default window title to reset it when update is not wanted
220        self.default_window_title = self.window.get_title()
221
222        self.abbreviate = False
223
224        self.window.connect('focus-out-event', self.on_window_losefocus)
225
226        # Handling the delete-event of the main window to avoid
227        # problems when closing it.
228        def destroy(*args):
229            self.hide()
230            return True
231
232        def window_event(*args):
233            return self.window_event(*args)
234
235        self.window.connect('delete-event', destroy)
236        self.window.connect('window-state-event', window_event)
237
238        # this line is important to resize the main window and make it
239        # smaller.
240        # TODO PORT do we still need this?
241        # self.window.set_geometry_hints(min_width=1, min_height=1)
242
243        # special trick to avoid the "lost guake on Ubuntu 'Show Desktop'" problem.
244        # DOCK makes the window foundable after having being "lost" after "Show
245        # Desktop"
246        self.window.set_type_hint(Gdk.WindowTypeHint.DOCK)
247        # Restore back to normal behavior
248        self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
249
250        # loading and setting up configuration stuff
251        GSettingHandler(self)
252        Keybinder.init()
253        self.hotkeys = Keybinder
254        Keybindings(self)
255        self.load_config()
256
257        # adding the first tab on guake
258        self.add_tab()
259
260        if self.settings.general.get_boolean('start-fullscreen'):
261            self.fullscreen()
262
263        refresh_user_start(self.settings)
264
265        # Pop-up that shows that guake is working properly (if not
266        # unset in the preferences windows)
267        if self.settings.general.get_boolean('use-popup'):
268            key = self.settings.keybindingsGlobal.get_string('show-hide')
269            keyval, mask = Gtk.accelerator_parse(key)
270            label = Gtk.accelerator_get_label(keyval, mask)
271            filename = pixmapfile('guake-notification.png')
272            notifier.showMessage(
273                _("Guake Terminal"),
274                _("Guake is now running,\n"
275                  "press <b>{!s}</b> to use it.").format(xml_escape(label)), filename
276            )
277
278        log.info("Guake initialized")
279
280    # new color methods should be moved to the GuakeTerminal class
281
282    def _load_palette(self):
283        colorRGBA = Gdk.RGBA(0, 0, 0, 0)
284        paletteList = list()
285        for color in self.settings.styleFont.get_string("palette").split(':'):
286            colorRGBA.parse(color)
287            paletteList.append(colorRGBA.copy())
288        return paletteList
289
290    def _get_background_color(self, palette_list):
291        if len(palette_list) > 16:
292            bg_color = palette_list[17]
293        else:
294            bg_color = Gdk.RGBA(0, 0, 0, 0.9)
295
296        return self._apply_transparency_to_color(bg_color)
297
298    def _apply_transparency_to_color(self, bg_color):
299        transparency = self.settings.styleBackground.get_int('transparency')
300        if not self.transparency_toggled:
301            bg_color.alpha = 1 / 100 * transparency
302        else:
303            bg_color.alpha = 1
304        return bg_color
305
306    def set_background_color_from_settings(self):
307        self.set_colors_from_settings()
308
309    def get_bgcolor(self):
310        palette_list = self._load_palette()
311        return self._get_background_color(palette_list)
312
313    def get_fgcolor(self):
314        palette_list = self._load_palette()
315        if len(palette_list) > 16:
316            font_color = palette_list[16]
317        else:
318            font_color = Gdk.RGBA(0, 0, 0, 0)
319        return font_color
320
321    def set_colors_from_settings(self):
322        bg_color = self.get_bgcolor()
323        font_color = self.get_fgcolor()
324        palette_list = self._load_palette()
325
326        for i in self.notebook.iter_terminals():
327            i.set_color_foreground(font_color)
328            i.set_color_bold(font_color)
329            i.set_colors(font_color, bg_color, palette_list[:16])
330
331    def set_bgcolor(self, bgcolor):
332        if isinstance(bgcolor, str):
333            c = Gdk.RGBA(0, 0, 0, 0)
334            log.debug("Building Gdk Color from: %r", bgcolor)
335            c.parse("#" + bgcolor)
336            bgcolor = c
337        if not isinstance(bgcolor, Gdk.RGBA):
338            raise TypeError("color should be Gdk.RGBA, is: {!r}".format(bgcolor))
339        bgcolor = self._apply_transparency_to_color(bgcolor)
340        log.debug("setting background color to: %r", bgcolor)
341        page_num = self.notebook.get_current_page()
342        for terminal in self.notebook.get_nth_page(page_num).iter_terminals():
343            terminal.set_color_background(bgcolor)
344
345    def set_fgcolor(self, fgcolor):
346        if isinstance(fgcolor, str):
347            c = Gdk.RGBA(0, 0, 0, 0)
348            log.debug("Building Gdk Color from: %r", fgcolor)
349            c.parse("#" + fgcolor)
350            fgcolor = c
351        if not isinstance(fgcolor, Gdk.RGBA):
352            raise TypeError("color should be Gdk.RGBA, is: {!r}".format(fgcolor))
353        log.debug("setting background color to: %r", fgcolor)
354        page_num = self.notebook.get_current_page()
355        for terminal in self.notebook.get_nth_page(page_num).iter_terminals():
356            terminal.set_color_foreground(fgcolor)
357
358    def execute_command(self, command, tab=None):
359        # TODO DBUS_ONLY
360        """Execute the `command' in the `tab'. If tab is None, the
361        command will be executed in the currently selected
362        tab. Command should end with '\n', otherwise it will be
363        appended to the string.
364        """
365        # TODO CONTEXTMENU this has to be rewriten and only serves the
366        # dbus interface, maybe this should be moved to dbusinterface.py
367        if not self.notebook.has_page():
368            self.add_tab()
369
370        if command[-1] != '\n':
371            command += '\n'
372
373        terminal = self.notebook.get_current_terminal()
374        terminal.feed_child(command)
375
376    def execute_command_by_uuid(self, tab_uuid, command):
377        # TODO DBUS_ONLY
378        """Execute the `command' in the tab whose terminal has the `tab_uuid' uuid
379        """
380        if command[-1] != '\n':
381            command += '\n'
382        try:
383            tab_uuid = uuid.UUID(tab_uuid)
384            page_index, = (
385                index for index, t in enumerate(self.notebook.iter_terminals())
386                if t.get_uuid() == tab_uuid
387            )
388        except ValueError:
389            pass
390        else:
391            terminals = self.notebook.get_terminals_for_page(page_index)
392            for current_vte in terminals:
393                current_vte.feed_child(command)
394
395    def on_window_losefocus(self, window, event):
396        """Hides terminal main window when it loses the focus and if
397        the window_losefocus gconf variable is True.
398        """
399        if not HidePrevention(self.window).may_hide():
400            return
401
402        value = self.settings.general.get_boolean('window-losefocus')
403        visible = window.get_property('visible')
404        self.losefocus_time = get_server_time(self.window)
405        if visible and value:
406            log.info("Hiding on focus lose")
407            self.hide()
408
409    def show_menu(self, status_icon, button, activate_time):
410        """Show the tray icon menu.
411        """
412        menu = self.get_widget('tray-menu')
413        menu.popup(None, None, None, Gtk.StatusIcon.position_menu, button, activate_time)
414
415    def show_about(self, *args):
416        # TODO DBUS ONLY
417        # TODO TRAY ONLY
418        """Hides the main window and creates an instance of the About
419        Dialog.
420        """
421        self.hide()
422        AboutDialog()
423
424    def show_prefs(self, *args):
425        # TODO DBUS ONLY
426        # TODO TRAY ONLY
427        """Hides the main window and creates an instance of the
428        Preferences window.
429        """
430        self.hide()
431        PrefsDialog(self.settings).show()
432
433    def is_iconified(self):
434        # TODO this is "dead" code only gets called to log output or in out commented code
435        if self.window:
436            cur_state = int(self.window.get_state())
437            return bool(cur_state & GDK_WINDOW_STATE_ICONIFIED)
438        return False
439
440    def window_event(self, window, event):
441        state = event.new_window_state
442        log.debug("Received window state event: %s", state)
443
444    def show_hide(self, *args):
445        """Toggles the main window visibility
446        """
447        log.debug("Show_hide called")
448        if self.forceHide:
449            self.forceHide = False
450            return
451
452        if not HidePrevention(self.window).may_hide():
453            return
454
455        if not self.win_prepare():
456            return
457
458        if not self.window.get_property('visible'):
459            log.info("Showing the terminal")
460            self.show()
461            self.set_terminal_focus()
462            return
463
464        # Disable the focus_if_open feature
465        #  - if doesn't work seamlessly on all system
466        #  - self.window.window.get_state doesn't provides us the right information on all
467        #    systems, especially on MATE/XFCE
468        #
469        # if self.client.get_bool(KEY('/general/focus_if_open')):
470        #     restore_focus = False
471        #     if self.window.window:
472        #         state = int(self.window.window.get_state())
473        #         if ((state & GDK_WINDOW_STATE_STICKY or
474        #                 state & GDK_WINDOW_STATE_WITHDRAWN
475        #              )):
476        #             restore_focus = True
477        #     else:
478        #         restore_focus = True
479        # if not self.hidden:
480        # restore_focus = True
481        #     if restore_focus:
482        #         log.debug("DBG: Restoring the focus to the terminal")
483        #         self.hide()
484        #         self.show()
485        #         self.window.window.focus()
486        #         self.set_terminal_focus()
487        #         return
488
489        log.info("hiding the terminal")
490        self.hide()
491
492    def show_focus(self, *args):
493        self.win_prepare()
494        self.show()
495        self.set_terminal_focus()
496
497    def win_prepare(self, *args):
498        event_time = self.hotkeys.get_current_event_time()
499        if not self.settings.general.get_boolean('window-refocus') and \
500                self.window.get_window() and self.window.get_property('visible'):
501            pass
502        elif not self.settings.general.get_boolean('window-losefocus'):
503            if self.losefocus_time and self.losefocus_time < event_time:
504                if self.window.get_window() and self.window.get_property('visible'):
505                    log.debug("DBG: Restoring the focus to the terminal")
506                    self.window.get_window().focus(event_time)
507                    self.set_terminal_focus()
508                    self.losefocus_time = 0
509                    return False
510        elif self.losefocus_time and self.settings.general.get_boolean('window-losefocus'):
511            if self.losefocus_time >= event_time and \
512                    (self.losefocus_time - event_time) < 10:
513                self.losefocus_time = 0
514                return False
515
516        # limit rate at which the visibility can be toggled.
517        if self.prev_showhide_time and event_time and \
518                (event_time - self.prev_showhide_time) < 65:
519            return False
520        self.prev_showhide_time = event_time
521
522        log.debug("")
523        log.debug("=" * 80)
524        log.debug("Window display")
525        if self.window:
526            cur_state = int(self.window.get_state())
527            is_sticky = bool(cur_state & GDK_WINDOW_STATE_STICKY)
528            is_withdrawn = bool(cur_state & GDK_WINDOW_STATE_WITHDRAWN)
529            is_above = bool(cur_state & GDK_WINDOW_STATE_ABOVE)
530            is_iconified = self.is_iconified()
531            log.debug("gtk.gdk.WindowState = %s", cur_state)
532            log.debug("GDK_WINDOW_STATE_STICKY? %s", is_sticky)
533            log.debug("GDK_WINDOW_STATE_WITHDRAWN? %s", is_withdrawn)
534            log.debug("GDK_WINDOW_STATE_ABOVE? %s", is_above)
535            log.debug("GDK_WINDOW_STATE_ICONIFIED? %s", is_iconified)
536            return True
537        return False
538
539    def show(self):
540        """Shows the main window and grabs the focus on it.
541        """
542        self.hidden = False
543
544        # setting window in all desktops
545
546        window_rect = RectCalculator.set_final_window_rect(self.settings, self.window)
547        self.get_widget('window-root').stick()
548
549        # add tab must be called before window.show to avoid a
550        # blank screen before adding the tab.
551        if not self.notebook.has_page():
552            self.add_tab()
553
554        self.window.set_keep_below(False)
555        self.window.show_all()
556        # this is needed because self.window.show_all() results in showing every
557        # thing which includes the scrollbar too
558        self.settings.general.triggerOnChangedValue(self.settings.general, "use-scrollbar")
559
560        # move the window even when in fullscreen-mode
561        log.debug("Moving window to: %r", window_rect)
562        self.window.move(window_rect.x, window_rect.y)
563
564        # this works around an issue in fluxbox
565        if not FullscreenManager(self.settings, self.window).is_fullscreen():
566            self.settings.general.triggerOnChangedValue(self.settings.general, 'window-height')
567
568        time = get_server_time(self.window)
569
570        # TODO PORT this
571        # When minized, the window manager seems to refuse to resume
572        # log.debug("self.window: %s. Dir=%s", type(self.window), dir(self.window))
573        # is_iconified = self.is_iconified()
574        # if is_iconified:
575        #     log.debug("Is iconified. Ubuntu Trick => "
576        #               "removing skip_taskbar_hint and skip_pager_hint "
577        #               "so deiconify can work!")
578        #     self.get_widget('window-root').set_skip_taskbar_hint(False)
579        #     self.get_widget('window-root').set_skip_pager_hint(False)
580        #     self.get_widget('window-root').set_urgency_hint(False)
581        #     log.debug("get_skip_taskbar_hint: {}".format(
582        #         self.get_widget('window-root').get_skip_taskbar_hint()))
583        #     log.debug("get_skip_pager_hint: {}".format(
584        #         self.get_widget('window-root').get_skip_pager_hint()))
585        #     log.debug("get_urgency_hint: {}".format(
586        #         self.get_widget('window-root').get_urgency_hint()))
587        #     glib.timeout_add_seconds(1, lambda: self.timeout_restore(time))
588        #
589
590        log.debug("order to present and deiconify")
591        self.window.present()
592        self.window.deiconify()
593        self.window.show()
594        self.window.get_window().focus(time)
595        self.window.set_type_hint(Gdk.WindowTypeHint.DOCK)
596        self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
597
598        # log.debug("Restoring skip_taskbar_hint and skip_pager_hint")
599        # if is_iconified:
600        #     self.get_widget('window-root').set_skip_taskbar_hint(False)
601        #     self.get_widget('window-root').set_skip_pager_hint(False)
602        #     self.get_widget('window-root').set_urgency_hint(False)
603
604        # This is here because vte color configuration works only after the
605        # widget is shown.
606
607        self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, 'color')
608        self.settings.styleBackground.triggerOnChangedValue(self.settings.styleBackground, 'color')
609
610        log.debug("Current window position: %r", self.window.get_position())
611
612        self.execute_hook('show')
613
614    def hide_from_remote(self):
615        """
616        Hides the main window of the terminal and sets the visible
617        flag to False.
618        """
619        log.debug("hide from remote")
620        self.forceHide = True
621        self.hide()
622
623    def show_from_remote(self):
624        """Show the main window of the terminal and sets the visible
625        flag to False.
626        """
627        log.debug("show from remote")
628        self.forceHide = True
629        self.show()
630
631    def hide(self):
632        """Hides the main window of the terminal and sets the visible
633        flag to False.
634        """
635        if not HidePrevention(self.window).may_hide():
636            return
637        self.hidden = True
638        self.get_widget('window-root').unstick()
639        self.window.hide()  # Don't use hide_all here!
640
641    def force_move_if_shown(self):
642        if not self.hidden:
643            # when displayed, GTK might refuse to move the window (X or Y position). Just hide and
644            # redisplay it so the final position is correct
645            log.debug("FORCING HIDE")
646            self.hide()
647            log.debug("FORCING SHOW")
648            self.show()
649
650    # -- configuration --
651
652    def load_config(self):
653        """"Just a proxy for all the configuration stuff.
654        """
655
656        self.settings.general.triggerOnChangedValue(self.settings.general, 'use-trayicon')
657        self.settings.general.triggerOnChangedValue(self.settings.general, 'prompt-on-quit')
658        self.settings.general.triggerOnChangedValue(self.settings.general, 'prompt-on-close-tab')
659        self.settings.general.triggerOnChangedValue(self.settings.general, 'window-tabbar')
660        self.settings.general.triggerOnChangedValue(self.settings.general, 'mouse-display')
661        self.settings.general.triggerOnChangedValue(self.settings.general, 'display-n')
662        self.settings.general.triggerOnChangedValue(self.settings.general, 'window-ontop')
663        if not FullscreenManager(self.settings, self.window).is_fullscreen():
664            self.settings.general.triggerOnChangedValue(self.settings.general, 'window-height')
665            self.settings.general.triggerOnChangedValue(self.settings.general, 'window-width')
666        self.settings.general.triggerOnChangedValue(self.settings.general, 'use-scrollbar')
667        self.settings.general.triggerOnChangedValue(self.settings.general, 'history-size')
668        self.settings.general.triggerOnChangedValue(self.settings.general, 'infinite-history')
669        self.settings.general.triggerOnChangedValue(self.settings.general, 'use-vte-titles')
670        self.settings.general.triggerOnChangedValue(self.settings.general, 'set-window-title')
671        self.settings.general.triggerOnChangedValue(self.settings.general, 'abbreviate-tab-names')
672        self.settings.general.triggerOnChangedValue(self.settings.general, 'max-tab-name-length')
673        self.settings.general.triggerOnChangedValue(self.settings.general, 'quick-open-enable')
674        self.settings.general.triggerOnChangedValue(
675            self.settings.general, 'quick-open-command-line'
676        )
677        self.settings.style.triggerOnChangedValue(self.settings.style, 'cursor-shape')
678        self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, 'style')
679        self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, 'palette')
680        self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, 'palette-name')
681        self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, 'allow-bold')
682        self.settings.styleBackground.triggerOnChangedValue(
683            self.settings.styleBackground, 'transparency'
684        )
685        self.settings.general.triggerOnChangedValue(self.settings.general, 'use-default-font')
686        self.settings.general.triggerOnChangedValue(self.settings.general, 'compat-backspace')
687        self.settings.general.triggerOnChangedValue(self.settings.general, 'compat-delete')
688
689    def accel_quit(self, *args):
690        """Callback to prompt the user whether to quit Guake or not.
691        """
692        procs = self.notebook.get_running_fg_processes_count()
693        tabs = self.notebook.get_n_pages()
694        prompt_cfg = self.settings.general.get_boolean('prompt-on-quit')
695        prompt_tab_cfg = self.settings.general.get_int('prompt-on-close-tab')
696        # "Prompt on tab close" config overrides "prompt on quit" config
697        if prompt_cfg or (prompt_tab_cfg == 1 and procs > 0) or (prompt_tab_cfg == 2):
698            log.debug("Remaining procs=%r", procs)
699            if PromptQuitDialog(self.window, procs, tabs).quit():
700                log.info("Quitting Guake")
701                Gtk.main_quit()
702        else:
703            log.info("Quitting Guake")
704            Gtk.main_quit()
705
706    def accel_reset_terminal(self, *args):
707        # TODO KEYBINDINGS ONLY
708        """Callback to reset and clean the terminal"""
709        HidePrevention(self.window).prevent()
710        current_term = self.notebook.get_current_terminal()
711        current_term.reset(True, True)
712        HidePrevention(self.window).allow()
713        return True
714
715    def accel_zoom_in(self, *args):
716        """Callback to zoom in.
717        """
718        for term in self.notebook.iter_terminals():
719            term.increase_font_size()
720        return True
721
722    def accel_zoom_out(self, *args):
723        """Callback to zoom out.
724        """
725        for term in self.notebook.iter_terminals():
726            term.decrease_font_size()
727        return True
728
729    def accel_increase_height(self, *args):
730        """Callback to increase height.
731        """
732        height = self.settings.general.get_int('window-height')
733        self.settings.general.set_int('window-height', min(height + 2, 100))
734        return True
735
736    def accel_decrease_height(self, *args):
737        """Callback to decrease height.
738        """
739        height = self.settings.general.get_int('window-height')
740        self.settings.general.set_int('window-height', max(height - 2, 0))
741        return True
742
743    def accel_increase_transparency(self, *args):
744        """Callback to increase transparency.
745        """
746        transparency = self.settings.styleBackground.get_int('transparency')
747        if int(transparency) - 2 > 0:
748            self.settings.styleBackground.set_int('transparency', int(transparency) - 2)
749        return True
750
751    def accel_decrease_transparency(self, *args):
752        """Callback to decrease transparency.
753        """
754        transparency = self.settings.styleBackground.get_int('transparency')
755        if int(transparency) + 2 < MAX_TRANSPARENCY:
756            self.settings.styleBackground.set_int('transparency', int(transparency) + 2)
757        return True
758
759    def accel_toggle_transparency(self, *args):
760        """Callback to toggle transparency.
761        """
762        self.transparency_toggled = not self.transparency_toggled
763        self.settings.styleBackground.triggerOnChangedValue(
764            self.settings.styleBackground, 'transparency'
765        )
766        return True
767
768    def accel_add(self, *args):
769        """Callback to add a new tab. Called by the accel key.
770        """
771        self.add_tab()
772        return True
773
774    def accel_prev(self, *args):
775        """Callback to go to the previous tab. Called by the accel key.
776        """
777        if self.notebook.get_current_page() == 0:
778            self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
779        else:
780            self.notebook.prev_page()
781        return True
782
783    def accel_next(self, *args):
784        """Callback to go to the next tab. Called by the accel key.
785        """
786        if self.notebook.get_current_page() + 1 == self.notebook.get_n_pages():
787            self.notebook.set_current_page(0)
788        else:
789            self.notebook.next_page()
790        return True
791
792    def accel_move_tab_left(self, *args):
793        # TODO KEYBINDINGS ONLY
794        """ Callback to move a tab to the left """
795        pos = self.notebook.get_current_page()
796        if pos != 0:
797            self.move_tab(pos, pos - 1)
798        return True
799
800    def accel_move_tab_right(self, *args):
801        # TODO KEYBINDINGS ONLY
802        """ Callback to move a tab to the right """
803        pos = self.notebook.get_current_page()
804        if pos != self.notebook.get_n_pages() - 1:
805            self.move_tab(pos, pos + 1)
806        return True
807
808    def move_tab(self, old_tab_pos, new_tab_pos):
809        self.notebook.reorder_child(self.notebook.get_nth_page(old_tab_pos), new_tab_pos)
810        self.notebook.set_current_page(new_tab_pos)
811
812    def gen_accel_switch_tabN(self, N):
813        """Generates callback (which called by accel key) to go to the Nth tab.
814        """
815
816        def callback(*args):
817            if 0 <= N < self.notebook.get_n_pages():
818                self.notebook.set_current_page(N)
819            return True
820
821        return callback
822
823    def accel_switch_tab_last(self, *args):
824        last_tab = self.notebook.get_n_pages() - 1
825        self.notebook.set_current_page(last_tab)
826        return True
827
828    def accel_rename_current_tab(self, *args):
829        """Callback to show the rename tab dialog. Called by the accel
830        key.
831        """
832        page_num = self.notebook.get_current_page()
833        page = self.notebook.get_nth_page(page_num)
834        self.notebook.get_tab_label(page).on_rename(None)
835        return True
836
837    def accel_copy_clipboard(self, *args):
838        # TODO KEYBINDINGS ONLY
839        """Callback to copy text in the shown terminal. Called by the
840        accel key.
841        """
842        self.notebook.get_current_terminal().copy_clipboard()
843        return True
844
845    def accel_paste_clipboard(self, *args):
846        # TODO KEYBINDINGS ONLY
847        """Callback to paste text in the shown terminal. Called by the
848        accel key.
849        """
850        self.notebook.get_current_terminal().paste_clipboard()
851        return True
852
853    def accel_toggle_hide_on_lose_focus(self, *args):
854        """Callback toggle whether the window should hide when it loses
855        focus. Called by the accel key.
856        """
857        if self.settings.general.get_boolean('window-losefocus'):
858            self.settings.general.set_boolean('window-losefocus', False)
859        else:
860            self.settings.general.set_boolean('window-losefocus', True)
861        return True
862
863    def accel_toggle_fullscreen(self, *args):
864        FullscreenManager(self.settings, self.window).toggle()
865        return True
866
867    def fullscreen(self):
868        FullscreenManager(self.settings, self.window).fullscreen()
869
870    def unfullscreen(self):
871        FullscreenManager(self.settings, self.window).unfullscreen()
872
873    # -- callbacks --
874
875    def recompute_tabs_titles(self):
876        """Updates labels on all tabs. This is required when `self.abbreviate`
877        changes
878        """
879        use_vte_titles = self.settings.general.get_boolean("use-vte-titles")
880        if not use_vte_titles:
881            return
882
883        # TODO NOTEBOOK this code only works if there is only one terminal in a
884        # page, this need to be rewritten
885        for terminal in self.notebook.iter_terminals():
886            page_num = self.notebook.page_num(terminal.get_parent())
887            self.notebook.rename_page(page_num, self.compute_tab_title(terminal), False)
888
889    def compute_tab_title(self, vte):
890        """Abbreviate and cut vte terminal title when necessary
891        """
892        vte_title = vte.get_window_title() or _("Terminal")
893        try:
894            current_directory = vte.get_current_directory()
895            if self.abbreviate and vte_title.endswith(current_directory):
896                parts = current_directory.split('/')
897                parts = [s[:1] for s in parts[:-1]] + [parts[-1]]
898                vte_title = vte_title[:len(vte_title) - len(current_directory)] + '/'.join(parts)
899        except OSError:
900            pass
901        return TabNameUtils.shorten(vte_title, self.settings)
902
903    def on_terminal_title_changed(self, vte, term):
904        # box must be a page
905        box = term.get_parent().get_root_box()
906        use_vte_titles = self.settings.general.get_boolean('use-vte-titles')
907        if not use_vte_titles:
908            return
909        # this may return -1, should be checked ;)
910        page_num = self.notebook.page_num(box)
911        # if tab has been renamed by user, don't override.
912        if not getattr(box, 'custom_label_set', False):
913            title = self.compute_tab_title(vte)
914            self.notebook.rename_page(page_num, title, False)
915            self.update_window_title(title)
916        else:
917            text = self.notebook.get_tab_text_page(box)
918            if text:
919                self.update_window_title(text)
920
921    def update_window_title(self, title):
922        if self.settings.general.get_boolean('set-window-title') is True:
923            self.window.set_title(title)
924        else:
925            self.window.set_title(self.default_window_title)
926
927    # TODO PORT reimplement drag and drop text on terminal
928
929    # -- tab related functions --
930
931    def close_tab(self, *args):
932        """Closes the current tab.
933        """
934        self.notebook.delete_page_current()
935
936    def delete_tab(self, page_num, kill=True, prompt=True):
937        """This function will destroy the notebook page, terminal and
938        tab widgets and will call the function to kill interpreter
939        forked by vte.
940        """
941        self.notebook.delete_page(page_num, kill=kill, prompt=prompt)
942
943    def rename_tab_uuid(self, term_uuid, new_text, user_set=True):
944        """Rename an already added tab by its UUID
945        """
946        term_uuid = uuid.UUID(term_uuid)
947        page_index, = (
948            index for index, t in enumerate(self.notebook.iter_terminals())
949            if t.get_uuid() == term_uuid
950        )
951        self.notebook.rename_page(page_index, new_text, user_set)
952
953    def rename_current_tab(self, new_text, user_set=False):
954        page_num = self.notebook.get_current_page()
955        self.notebook.rename_page(page_num, new_text, user_set)
956
957    def terminal_spawned(self, notebook, terminal, pid):
958        self.load_config()
959        terminal.connect('window-title-changed', self.on_terminal_title_changed, terminal)
960
961    def add_tab(self, directory=None):
962        """Adds a new tab to the terminal notebook.
963        """
964        self.notebook.new_page_with_focus(directory)
965
966    def find_tab(self, directory=None):
967        log.debug("find")
968        # TODO SEARCH
969        HidePrevention(self.window).prevent()
970        search_text = Gtk.TextView()
971
972        dialog = Gtk.Dialog(
973            _("Find"), self.window, Gtk.DialogFlags.DESTROY_WITH_PARENT,
974            (
975                _("Forward"), RESPONSE_FORWARD, _("Backward"), RESPONSE_BACKWARD, Gtk.STOCK_CANCEL,
976                Gtk.ResponseType.NONE
977            )
978        )
979        dialog.vbox.pack_end(search_text, True, True, 0)
980        dialog.buffer = search_text.get_buffer()
981        dialog.connect("response", self._dialog_response_callback)
982
983        search_text.show()
984        search_text.grab_focus()
985        dialog.show_all()
986        # Note: beware to reset preventHide when closing the find dialog
987
988    def _dialog_response_callback(self, dialog, response_id):
989        if response_id not in (RESPONSE_FORWARD, RESPONSE_BACKWARD):
990            dialog.destroy()
991            HidePrevention(self.window).allow()
992            return
993
994        start, end = dialog.buffer.get_bounds()
995        search_string = start.get_text(end)
996
997        log.debug(
998            "Searching for %r %s\n", search_string, "forward"
999            if response_id == RESPONSE_FORWARD else "backward"
1000        )
1001
1002        current_term = self.notebook.get_current_terminal()
1003        log.debug("type: %r", type(current_term))
1004        log.debug("dir: %r", dir(current_term))
1005        current_term.search_set_gregex()
1006        current_term.search_get_gregex()
1007
1008        # buffer = self.text_view.get_buffer()
1009        # if response_id == RESPONSE_FORWARD:
1010        #     buffer.search_forward(search_string, self)
1011        # elif response_id == RESPONSE_BACKWARD:
1012        #     buffer.search_backward(search_string, self)
1013
1014    def page_deleted(self, *args):
1015        if not self.notebook.has_page():
1016            self.hide()
1017            # avoiding the delay on next Guake show request
1018            self.add_tab()
1019        else:
1020            self.set_terminal_focus()
1021
1022        self.was_deleted_tab = True
1023        abbreviate_tab_names = self.settings.general.get_boolean('abbreviate-tab-names')
1024        if abbreviate_tab_names:
1025            self.abbreviate = False
1026            self.recompute_tabs_titles()
1027
1028    def set_terminal_focus(self):
1029        """Grabs the focus on the current tab.
1030        """
1031        self.notebook.set_current_page(self.notebook.get_current_page())
1032
1033    def get_selected_uuidtab(self):
1034        # TODO DBUS ONLY
1035        """Returns the uuid of the current selected terminal
1036        """
1037        page_num = self.notebook.get_current_page()
1038        terminals = self.notebook.get_terminals_for_page(page_num)
1039        return str(terminals[0].get_uuid())
1040
1041    def search_on_web(self, *args):
1042        """Search for the selected text on the web
1043        """
1044        # TODO KEYBINDINGS ONLY
1045        current_term = self.notebook.get_current_terminal()
1046
1047        if current_term.get_has_selection():
1048            current_term.copy_clipboard()
1049            guake_clipboard = Gtk.Clipboard.get_default(self.window.get_display())
1050            search_query = guake_clipboard.wait_for_text()
1051            search_query = quote_plus(search_query)
1052            if search_query:
1053                # TODO search provider should be selectable (someone might
1054                # prefer bing.com, the internet is a strange place ¯\_(ツ)_/¯ )
1055                search_url = "https://www.google.com/#q={!s}&safe=off".format(search_query, )
1056                Gtk.show_uri(self.window.get_screen(), search_url, get_server_time(self.window))
1057        return True
1058
1059    def set_tab_position(self, *args):
1060        if self.settings.general.get_boolean('tab-ontop'):
1061            self.notebook.set_tab_pos(Gtk.PositionType.TOP)
1062        else:
1063            self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM)
1064
1065    def execute_hook(self, event_name):
1066        """Execute shell commands related to current event_name"""
1067        hook = self.settings.hooks.get_string('{!s}'.format(event_name))
1068        if hook is not None and hook != "":
1069            hook = hook.split()
1070            try:
1071                subprocess.Popen(hook)
1072            except OSError as oserr:
1073                if oserr.errno == 8:
1074                    log.error("Hook execution failed! Check shebang at first line of %s!", hook)
1075                    log.debug(traceback.format_exc())
1076                else:
1077                    log.error(str(oserr))
1078            except Exception as e:
1079                log.error("hook execution failed! %s", e)
1080                log.debug(traceback.format_exc())
1081            else:
1082                log.debug("hook on event %s has been executed", event_name)
1083