Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Bugs? Information?
`improving its translations in your language <https://hosted.weblate.org/projects/guake/guake/>`_.
Guake users are welcome `to support Weblate <https://weblate.org/donate/>`_ in providing this
service for free for OpenSource Projects.
- You may improve Guake, but first please
read `Contributing Guide <https://guake.readthedocs.io/en/stable/contributing/index.html>`_.

**Important note**: Do **NOT** use the domain guake.org, it has been registered by someone outside
the team. We cannot be held responsible for the content on that web site.
Expand Down
45 changes: 36 additions & 9 deletions guake/boxes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import enum
import logging
import time

Expand Down Expand Up @@ -26,6 +27,14 @@

log = logging.getLogger(__name__)


# Foreground color applied to a background tab's title when it has unseen
# activity. Chosen to stay readable on both light and dark tab bars.
TAB_ACTIVITY_COLOR = "#E8A33D"
# Foreground color applied once unseen activity has gone quiet for a while
TAB_ACTIVITY_STALE_COLOR = "#3D8AE8"


# TODO remove calls to guake


Expand Down Expand Up @@ -637,17 +646,17 @@ def remove_dead_child(self, child):
print("I have never seen this widget!")


# Foreground color applied to a background tab's title when it has unseen
# activity. Chosen to stay readable on both light and dark tab bars.
TAB_ACTIVITY_COLOR = "#E8A33D"
class TabActivityState(enum.Enum):
ACTIVE = "active"
STALE = "stale"


class TabLabelEventBox(Gtk.EventBox):
def __init__(self, notebook, text, settings):
super().__init__()
self.notebook = notebook
self._text = text
self._activity = False
self._activity_state = None
self.box = Gtk.Box(homogeneous=Gtk.Orientation.HORIZONTAL, spacing=0, visible=True)
self.label = Gtk.Label(label=text, visible=True)
self.close_button = Gtk.Button(
Expand Down Expand Up @@ -676,22 +685,40 @@ def set_activity(self, active):
Returns True if the activity state actually changed, so callers can
avoid redundant re-rendering on the frequent contents-changed signal.
"""
active = bool(active)
if active == self._activity:
return self.set_activity_state(TabActivityState.ACTIVE if active else None)

def set_activity_state(self, state):
"""Set the activity indicator state: None (no activity),
TabActivityState.ACTIVE (fresh, unseen output) or
TabActivityState.STALE (unseen output, but quiet for a while - see
tab-activity-cooldown).

Returns True if the state actually changed, so callers can avoid
redundant re-rendering on the frequent contents-changed signal.
"""
if state == self._activity_state:
return False
self._activity = active
self._activity_state = state
self._render()
return True

def get_activity(self):
return self._activity
return self._activity_state is not None

def get_activity_state(self):
return self._activity_state

def _render(self):
if self._activity:
if self._activity_state == TabActivityState.ACTIVE:
self.label.set_markup(
f'<span foreground="{TAB_ACTIVITY_COLOR}" weight="bold">'
f"{GLib.markup_escape_text(self._text)}</span>"
)
elif self._activity_state == TabActivityState.STALE:
self.label.set_markup(
f'<span foreground="{TAB_ACTIVITY_STALE_COLOR}" weight="bold">'
f"{GLib.markup_escape_text(self._text)}</span>"
)
else:
self.label.set_text(self._text)

Expand Down
10 changes: 0 additions & 10 deletions guake/data/org.guake.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,6 @@
<summary>Highlight tabs with activity</summary>
<description>If true, the title of a background tab is highlighted when its terminal produces output, until the tab is selected.</description>
</key>
<key name="tab-activity-new-tab-grace" type="d">
<default>3.0</default>
<summary>Activity grace period for new tabs</summary>
<description>Number of seconds during which activity is ignored on a newly created tab, to avoid highlighting it because of its initial shell prompt.</description>
</key>
<key name="tab-activity-focus-loss-grace" type="d">
<default>1.0</default>
<summary>Activity grace period after losing focus</summary>
<description>Number of seconds during which activity is ignored on a tab right after it loses focus.</description>
</key>
<key name="window-width" type="i">
<default>100</default>
<summary>Window width.</summary>
Expand Down
84 changes: 0 additions & 84 deletions guake/data/prefs.glade
Original file line number Diff line number Diff line change
Expand Up @@ -423,90 +423,6 @@
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox_tab_activity_new_tab_grace">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="lbl_tab_activity_new_tab_grace">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Ignore activity on new tabs for (seconds):</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="tab_activity_new_tab_grace">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="text" translatable="yes">3.0</property>
<property name="adjustment">tab_activity_new_tab_grace_adj</property>
<property name="climb-rate">1</property>
<property name="digits">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="on_tab_activity_new_tab_grace_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">6</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox_tab_activity_focus_loss_grace">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="lbl_tab_activity_focus_loss_grace">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Ignore activity after losing focus for (seconds):</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="tab_activity_focus_loss_grace">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="text" translatable="yes">1.0</property>
<property name="adjustment">tab_activity_focus_loss_grace_adj</property>
<property name="climb-rate">1</property>
<property name="digits">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="on_tab_activity_focus_loss_grace_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">7</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
Expand Down
59 changes: 49 additions & 10 deletions guake/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from guake.about import AboutDialog
from guake.boxes import RootTerminalBox
from guake.boxes import TabActivityState
from guake.boxes import TabLabelEventBox
from guake.boxes import TerminalBox
from guake.callbacks import MenuHideCallback
Expand All @@ -40,6 +41,7 @@

gi.require_version("Gtk", "3.0")
gi.require_version("Wnck", "3.0")
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
Expand All @@ -51,6 +53,16 @@

log = logging.getLogger(__name__)

# Number of seconds during which activity is ignored on a newly created tab,
# to avoid highlighting it because of its initial shell prompt.
TAB_ACTIVITY_NEW_TAB_GRACE = 3.0
# Number of seconds during which activity is ignored on a tab right after it
# loses focus.
TAB_ACTIVITY_FOCUS_LOSS_GRACE = 1.0
# Number of seconds of inactivity after which a highlighted tab's indicator
# fades from active to stale color.
TAB_ACTIVITY_COOLDOWN = 30.0


class TerminalNotebook(Gtk.Notebook):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -350,6 +362,10 @@ def delete_page(self, page_num, kill=True, prompt=0):

@save_tabs_when_changed
def remove_page(self, page_num):
page = self.get_nth_page(page_num)
source_id = getattr(page, "activity_stale_source_id", None)
if source_id is not None:
GLib.source_remove(source_id)
super().remove_page(page_num)
# focusing the first terminal on the previous page
if self.get_current_page() > -1:
Expand Down Expand Up @@ -379,8 +395,7 @@ def new_page(self, directory=None, position=None, empty=False, open_tab_cwd=Fals
# initial shell prompt (especially on session restore) does not light up
# every background tab at once.
if self.guake:
grace = self.guake.settings.general.get_double("tab-activity-new-tab-grace")
root_terminal_box.activity_ignore_until = time.monotonic() + grace
root_terminal_box.activity_ignore_until = time.monotonic() + TAB_ACTIVITY_NEW_TAB_GRACE
page_num = self.insert_page(
root_terminal_box, None, position if position is not None else -1
)
Expand Down Expand Up @@ -450,34 +465,58 @@ def on_terminal_activity(self, terminal):
page = self.get_nth_page(page_index)
if time.monotonic() < getattr(page, "activity_ignore_until", 0):
return
page.activity_deadline = time.monotonic() + TAB_ACTIVITY_COOLDOWN
label = self.get_tab_label(page)
if hasattr(label, "set_activity_state"):
label.set_activity_state(TabActivityState.ACTIVE)
# Only arm a timer if this page doesn't already have one pending: further
# activity just pushes activity_deadline out, the live timer picks that up
# when it wakes rather than being cancelled and re-armed on every event.
if getattr(page, "activity_stale_source_id", None) is None:
self._schedule_stale_check(page, TAB_ACTIVITY_COOLDOWN)

def _schedule_stale_check(self, page, delay):
page.activity_stale_source_id = GLib.timeout_add(
max(0, int(delay * 1000)), self._on_activity_stale_check, page
)

def _on_activity_stale_check(self, page):
"""Fade a tab from ACTIVE (orange) to STALE (blue) once its cooldown has
elapsed. If activity pushed the deadline further out while this was
pending, reschedule for the remaining time instead of fading early."""
page.activity_stale_source_id = None
remaining = getattr(page, "activity_deadline", 0) - time.monotonic()
if remaining > 0:
self._schedule_stale_check(page, remaining)
return False
label = self.get_tab_label(page)
if hasattr(label, "set_activity"):
label.set_activity(True)
if hasattr(label, "set_activity_state"):
label.set_activity_state(TabActivityState.STALE)
return False

def on_switch_page(self, notebook, page, page_num):
"""Clear the highlight from the tab being switched to, and start a short
grace period on the tab that just lost focus."""
previous_page = self._activity_last_page
self._activity_last_page = page
if previous_page is not None and previous_page is not page and getattr(self, "guake", None):
grace = self.guake.settings.general.get_double("tab-activity-focus-loss-grace")
# Never shorten an existing (e.g. longer new-tab) grace: on session
# restore every tab briefly becomes current, and clobbering the
# creation grace with this one would let the startup burst through.
previous_page.activity_ignore_until = max(
getattr(previous_page, "activity_ignore_until", 0),
time.monotonic() + grace,
time.monotonic() + TAB_ACTIVITY_FOCUS_LOSS_GRACE,
)
label = self.get_tab_label(page)
if hasattr(label, "set_activity"):
label.set_activity(False)
if hasattr(label, "set_activity_state"):
label.set_activity_state(None)

def clear_all_tab_activity(self):
"""Remove activity highlights from every tab (e.g. when the feature is
disabled)."""
for label in self.iter_tabs():
if hasattr(label, "set_activity"):
label.set_activity(False)
if hasattr(label, "set_activity_state"):
label.set_activity_state(None)

def terminal_attached(self, terminal):
terminal.emit("focus", Gtk.DirectionType.TAB_FORWARD)
Expand Down
14 changes: 0 additions & 14 deletions guake/prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,14 +484,6 @@ def on_display_tab_activity_toggled(self, chk):
"""Changes the value of display_tab_activity in dconf"""
self.settings.general.set_boolean("display-tab-activity", chk.get_active())

def on_tab_activity_new_tab_grace_changed(self, spin):
"""Changes the value of tab_activity_new_tab_grace in dconf"""
self.settings.general.set_double("tab-activity-new-tab-grace", spin.get_value())

def on_tab_activity_focus_loss_grace_changed(self, spin):
"""Changes the value of tab_activity_focus_loss_grace in dconf"""
self.settings.general.set_double("tab-activity-focus-loss-grace", spin.get_value())

# scrolling tab

def on_use_scrollbar_toggled(self, chk):
Expand Down Expand Up @@ -1214,12 +1206,6 @@ def load_configs(self):
value = self.settings.general.get_boolean("display-tab-activity")
self.get_widget("display_tab_activity").set_active(value)

# tab activity grace periods
value = self.settings.general.get_double("tab-activity-new-tab-grace")
self.get_widget("tab_activity_new_tab_grace").set_value(value)
value = self.settings.general.get_double("tab-activity-focus-loss-grace")
self.get_widget("tab_activity_focus_loss_grace").set_value(value)

self._load_screen_settings()

value = self.settings.general.get_boolean("quick-open-enable")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
release_summary: >

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put everything you want to say in one reno file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Tab activity indicators now fade from an active color to a stale color after
a configurable cooldown period of no new output, so long-quiet tabs are
visually distinguished from tabs with fresh activity.

features:
- |
- add a stale/cooldown color stage to tab activity indication

15 changes: 0 additions & 15 deletions releasenotes/notes/tab_activity_indication-be54b262d370a96b.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,3 @@ features:
- |
- add tabs activity indication

known_issues:

upgrade:

deprecations:

security:

fixes:

translations:

notes_for_package_maintainers:

other:
Loading