mirror of
https://github.com/Guake/guake.git
synced 2025-10-26 11:27:13 +00:00
402 lines
15 KiB
Python
402 lines
15 KiB
Python
# -*- coding: utf-8; -*-
|
|
"""
|
|
Copyright (C) 2007-2018 Guake authors
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public
|
|
License along with this program; if not, write to the
|
|
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301 USA
|
|
"""
|
|
|
|
from guake.about import AboutDialog
|
|
from guake.boxes import DualTerminalBox
|
|
from guake.boxes import RootTerminalBox
|
|
from guake.boxes import TabLabelEventBox
|
|
from guake.boxes import TerminalBox
|
|
from guake.callbacks import MenuHideCallback
|
|
from guake.callbacks import NotebookScrollCallback
|
|
from guake.dialogs import PromptQuitDialog
|
|
from guake.menus import mk_notebook_context_menu
|
|
from guake.prefs import PrefsDialog
|
|
from guake.utils import save_tabs_when_changed
|
|
|
|
import gi
|
|
import os
|
|
gi.require_version('Gtk', '3.0')
|
|
gi.require_version('Wnck', '3.0')
|
|
from gi.repository import GObject
|
|
from gi.repository import Gdk
|
|
from gi.repository import Gtk
|
|
from gi.repository import Wnck
|
|
from guake.terminal import GuakeTerminal
|
|
from locale import gettext as _
|
|
|
|
import logging
|
|
import posix
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class TerminalNotebook(Gtk.Notebook):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
Gtk.Notebook.__init__(self, *args, **kwargs)
|
|
self.last_terminal_focused = None
|
|
|
|
self.set_name("notebook-teminals")
|
|
self.set_tab_pos(Gtk.PositionType.BOTTOM)
|
|
self.set_property("show-tabs", True)
|
|
self.set_property("enable-popup", False)
|
|
self.set_property("scrollable", True)
|
|
self.set_property("show-border", False)
|
|
self.set_property("visible", True)
|
|
self.set_property("has-focus", True)
|
|
self.set_property("can-focus", True)
|
|
self.set_property("is-focus", True)
|
|
self.set_property("expand", True)
|
|
|
|
if GObject.signal_lookup('terminal-spawned', TerminalNotebook) == 0:
|
|
GObject.signal_new(
|
|
'terminal-spawned', TerminalNotebook, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
|
|
(GObject.TYPE_PYOBJECT, GObject.TYPE_INT)
|
|
)
|
|
GObject.signal_new(
|
|
'page-deleted', TerminalNotebook, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()
|
|
)
|
|
|
|
self.scroll_callback = NotebookScrollCallback(self)
|
|
self.add_events(Gdk.EventMask.SCROLL_MASK)
|
|
self.connect('scroll-event', self.scroll_callback.on_scroll)
|
|
self.notebook_on_button_press_id = self.connect(
|
|
"button-press-event", self.on_button_press, None
|
|
)
|
|
|
|
self.new_page_button = Gtk.Button(
|
|
image=Gtk.Image.new_from_icon_name("tab-new", Gtk.IconSize.MENU), visible=True
|
|
)
|
|
self.new_page_button.connect("clicked", self.on_new_tab)
|
|
self.set_action_widget(self.new_page_button, Gtk.PackType.END)
|
|
|
|
def attach_guake(self, guake):
|
|
self.guake = guake
|
|
|
|
def on_button_press(self, target, event, user_data):
|
|
if event.button == 3:
|
|
menu = mk_notebook_context_menu(self)
|
|
menu.connect("hide", MenuHideCallback(self.guake.window).on_hide)
|
|
|
|
try:
|
|
menu.popup_at_pointer(event)
|
|
except AttributeError:
|
|
# Gtk 3.18 fallback ("'Menu' object has no attribute 'popup_at_pointer'")
|
|
menu.popup(None, None, None, None, event.button, event.time)
|
|
|
|
elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
|
|
self.new_page_with_focus()
|
|
|
|
return False
|
|
|
|
def set_last_terminal_focused(self, terminal):
|
|
self.last_terminal_focused = terminal
|
|
|
|
def get_focused_terminal(self):
|
|
for terminal in self.iter_terminals():
|
|
if terminal.has_focus():
|
|
return terminal
|
|
|
|
def get_current_terminal(self):
|
|
# TODO NOTEBOOK the name of this method should
|
|
# be changed, for now it returns the last focused terminal
|
|
return self.last_terminal_focused
|
|
|
|
def get_terminals_for_page(self, index):
|
|
page = self.get_nth_page(index)
|
|
return page.get_terminals()
|
|
|
|
def get_terminals(self):
|
|
terminals = []
|
|
for page in self.iter_pages():
|
|
terminals += page.get_terminals()
|
|
return terminals
|
|
|
|
def get_running_fg_processes_count(self):
|
|
fg_proc_count = 0
|
|
for page in self.iter_pages():
|
|
fg_proc_count += self.get_running_fg_processes_count_page(self.page_num(page))
|
|
return fg_proc_count
|
|
|
|
def get_running_fg_processes_count_page(self, index):
|
|
total_procs = 0
|
|
for terminal in self.get_terminals_for_page(index):
|
|
pty = terminal.get_pty()
|
|
if not pty:
|
|
continue
|
|
fdpty = pty.get_fd()
|
|
term_pid = terminal.pid
|
|
try:
|
|
fgpid = posix.tcgetpgrp(fdpty)
|
|
log.debug("found running pid: %s", fgpid)
|
|
if fgpid not in (-1, term_pid):
|
|
total_procs += 1
|
|
except OSError:
|
|
log.debug(
|
|
"Cannot retrieve any pid from terminal %s, looks like it is already dead", index
|
|
)
|
|
return 0
|
|
return total_procs
|
|
|
|
def has_page(self):
|
|
return self.get_n_pages() > 0
|
|
|
|
def iter_terminals(self):
|
|
for page in self.iter_pages():
|
|
if page is not None:
|
|
for t in page.iter_terminals():
|
|
yield t
|
|
|
|
def iter_tabs(self):
|
|
for page_num in range(self.get_n_pages()):
|
|
yield self.get_tab_label(self.get_nth_page(page_num))
|
|
|
|
def iter_pages(self):
|
|
for page_num in range(self.get_n_pages()):
|
|
yield self.get_nth_page(page_num)
|
|
|
|
def delete_page(self, page_num, kill=True, prompt=0):
|
|
if page_num >= self.get_n_pages() or page_num < 0:
|
|
log.debug("Can not delete page %s no such index", page_num)
|
|
return
|
|
# TODO NOTEBOOK it would be nice if none of the "ui" stuff
|
|
# (PromptQuitDialog) would be in here
|
|
procs = self.get_running_fg_processes_count_page(page_num)
|
|
if prompt == 2 or (prompt == 1 and procs > 0):
|
|
# TODO NOTEBOOK remove call to guake
|
|
if not PromptQuitDialog(self.guake.window, procs, -1).close_tab():
|
|
return
|
|
|
|
for terminal in self.get_terminals_for_page(page_num):
|
|
if kill:
|
|
terminal.kill()
|
|
terminal.destroy()
|
|
|
|
@save_tabs_when_changed
|
|
def remove_page(self, page_num):
|
|
super().remove_page(page_num)
|
|
# focusing the first terminal on the previous page
|
|
if self.get_current_page() > -1:
|
|
page = self.get_nth_page(self.get_current_page())
|
|
if page.get_terminals():
|
|
page.get_terminals()[0].grab_focus()
|
|
self.emit('page-deleted')
|
|
|
|
def delete_page_by_label(self, label, kill=True, prompt=0):
|
|
self.delete_page(self.find_tab_index_by_label(label), kill, prompt)
|
|
|
|
def delete_page_current(self, kill=True, prompt=0):
|
|
self.delete_page(self.get_current_page(), kill, prompt)
|
|
|
|
def new_page(self, directory=None):
|
|
terminal = self.terminal_spawn(directory)
|
|
terminal_box = TerminalBox()
|
|
terminal_box.set_terminal(terminal)
|
|
root_terminal_box = RootTerminalBox(self.guake, self)
|
|
root_terminal_box.set_child(terminal_box)
|
|
page_num = self.append_page(root_terminal_box, None)
|
|
self.set_tab_reorderable(root_terminal_box, True)
|
|
self.show_all() # needed to show newly added tabs and pages
|
|
# this is needed to initially set the last_terminal_focused,
|
|
# one could also call terminal.get_parent().on_terminal_focus()
|
|
self.terminal_attached(terminal)
|
|
return root_terminal_box, page_num, terminal
|
|
|
|
def terminal_spawn(self, directory=None):
|
|
terminal = GuakeTerminal(self.guake)
|
|
terminal.grab_focus()
|
|
if not isinstance(directory, str):
|
|
directory = os.environ['HOME']
|
|
try:
|
|
if self.guake.settings.general.get_boolean('open-tab-cwd'):
|
|
active_terminal = self.get_focused_terminal()
|
|
directory = os.path.expanduser('~')
|
|
if active_terminal:
|
|
directory = active_terminal.get_current_directory()
|
|
except Exception as e:
|
|
pass
|
|
log.info("Spawning new terminal at %s", directory)
|
|
terminal.spawn_sync_pid(directory)
|
|
return terminal
|
|
|
|
def terminal_attached(self, terminal):
|
|
terminal.emit("focus", Gtk.DirectionType.TAB_FORWARD)
|
|
self.emit('terminal-spawned', terminal, terminal.pid)
|
|
|
|
def new_page_with_focus(self, directory=None, label=None, user_set=False):
|
|
box, page_num, terminal = self.new_page(directory)
|
|
self.set_current_page(page_num)
|
|
if not label:
|
|
self.rename_page(page_num, _("Terminal"), False)
|
|
else:
|
|
self.rename_page(page_num, label, user_set)
|
|
terminal.grab_focus()
|
|
|
|
def rename_page(self, page_index, new_text, user_set=False):
|
|
"""Rename an already added page by its index. Use user_set to define
|
|
if the rename was triggered by the user (eg. rename dialog) or by
|
|
an update from the vte (eg. vte:window-title-changed)
|
|
"""
|
|
page = self.get_nth_page(page_index)
|
|
if not getattr(page, "custom_label_set", False) or user_set:
|
|
old_label = self.get_tab_label(page)
|
|
if isinstance(old_label, TabLabelEventBox):
|
|
old_label.set_text(new_text)
|
|
else:
|
|
label = TabLabelEventBox(self, new_text, self.guake.settings)
|
|
label.add_events(Gdk.EventMask.SCROLL_MASK)
|
|
label.connect('scroll-event', self.scroll_callback.on_scroll)
|
|
|
|
self.set_tab_label(page, label)
|
|
if user_set:
|
|
setattr(page, "custom_label_set", new_text != "-")
|
|
|
|
def find_tab_index_by_label(self, eventbox):
|
|
for index, tab_eventbox in enumerate(self.iter_tabs()):
|
|
if eventbox is tab_eventbox:
|
|
return index
|
|
return -1
|
|
|
|
def find_page_index_by_terminal(self, terminal):
|
|
for index, page in enumerate(self.iter_pages()):
|
|
for t in page.iter_terminals():
|
|
if t is terminal:
|
|
return index
|
|
return -1
|
|
|
|
def get_tab_text_index(self, index):
|
|
return self.get_tab_label(self.get_nth_page(index)).get_text()
|
|
|
|
def get_tab_text_page(self, page):
|
|
return self.get_tab_label(page).get_text()
|
|
|
|
@save_tabs_when_changed
|
|
def on_new_tab(self, user_data):
|
|
self.new_page_with_focus()
|
|
|
|
def on_show_preferences(self, user_data):
|
|
self.guake.hide()
|
|
PrefsDialog(self.guake.settings).show()
|
|
|
|
def on_show_about(self, user_data):
|
|
self.guake.hide()
|
|
AboutDialog()
|
|
|
|
def on_quit(self, user_data):
|
|
self.guake.accel_quit()
|
|
|
|
def on_save_tabs(self, user_data):
|
|
self.guake.save_tabs()
|
|
|
|
def on_restore_tabs(self, user_data):
|
|
self.guake.restore_tabs()
|
|
|
|
|
|
class NotebookManager(GObject.Object):
|
|
|
|
def __init__(
|
|
self, window, notebook_parent, workspaces_enabled, terminal_spawned_cb, page_deleted_cb
|
|
):
|
|
GObject.Object.__init__(self)
|
|
GObject.signal_new(
|
|
'notebook-created', self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
|
|
(GObject.TYPE_PYOBJECT, GObject.TYPE_INT)
|
|
)
|
|
self.current_notebook = 0
|
|
self.notebooks = {}
|
|
self.window = window
|
|
self.notebook_parent = notebook_parent
|
|
self.terminal_spawned_cb = terminal_spawned_cb
|
|
self.page_deleted_cb = page_deleted_cb
|
|
self.screen = Wnck.Screen.get_default()
|
|
if workspaces_enabled:
|
|
self.screen.connect("active-workspace-changed", self.workspace_changed)
|
|
|
|
def workspace_changed(self, screen, previous_workspace):
|
|
self.set_workspace(self.screen.get_active_workspace().get_number())
|
|
|
|
def get_notebook(self, workspace_index: int):
|
|
if not self.has_notebook_for_workspace(workspace_index):
|
|
self.notebooks[workspace_index] = TerminalNotebook()
|
|
self.emit('notebook-created', self.notebooks[workspace_index], workspace_index)
|
|
self.notebooks[workspace_index].connect('terminal-spawned', self.terminal_spawned_cb)
|
|
self.notebooks[workspace_index].connect('page-deleted', self.page_deleted_cb)
|
|
log.info("created fresh notebook for workspace %d", self.current_notebook)
|
|
|
|
# add a tab if there is none
|
|
if not self.notebooks[workspace_index].has_page():
|
|
self.notebooks[workspace_index].new_page_with_focus(None)
|
|
|
|
return self.notebooks[workspace_index]
|
|
|
|
def get_current_notebook(self):
|
|
return self.get_notebook(self.current_notebook)
|
|
|
|
def has_notebook_for_workspace(self, workspace_index):
|
|
return workspace_index in self.notebooks.keys()
|
|
|
|
def set_workspace(self, index: int):
|
|
self.notebook_parent.remove(self.get_current_notebook())
|
|
self.current_notebook = index
|
|
log.info("current workspace is %d", self.current_notebook)
|
|
notebook = self.get_current_notebook()
|
|
self.notebook_parent.add(notebook)
|
|
if self.window.get_property('visible') and \
|
|
notebook.last_terminal_focused is not None:
|
|
notebook.last_terminal_focused.grab_focus()
|
|
|
|
def get_notebooks(self):
|
|
return self.notebooks
|
|
|
|
def get_terminals(self):
|
|
terminals = []
|
|
for k in self.notebooks:
|
|
terminals += self.notebooks[k].get_terminals()
|
|
return terminals
|
|
|
|
def iter_terminals(self):
|
|
for k in self.notebooks:
|
|
for t in self.notebooks[k].iter_terminals():
|
|
yield t
|
|
|
|
def iter_pages(self):
|
|
for k in self.notebooks:
|
|
for t in self.notebooks[k].iter_pages():
|
|
yield t
|
|
|
|
def iter_notebooks(self):
|
|
for k in self.notebooks:
|
|
yield self.notebooks[k]
|
|
|
|
def get_n_pages(self):
|
|
n = 0
|
|
for k in self.notebooks:
|
|
n += self.notebooks[k].get_n_pages()
|
|
return n
|
|
|
|
def get_n_notebooks(self):
|
|
return len(self.notebooks.keys())
|
|
|
|
def get_running_fg_processes_count(self):
|
|
r_fg_c = 0
|
|
for k in self.notebooks:
|
|
r_fg_c += self.notebooks[k].get_running_fg_processes_count()
|
|
return r_fg_c
|