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