diff --git a/README.rst b/README.rst
index 445960b1d..7cb5c04d5 100644
--- a/README.rst
+++ b/README.rst
@@ -55,6 +55,8 @@ Bugs? Information?
`improving its translations in your language `_.
Guake users are welcome `to support Weblate `_ in providing this
service for free for OpenSource Projects.
+- You may improve Guake, but first please
+ read `Contributing Guide `_.
**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.
diff --git a/guake/boxes.py b/guake/boxes.py
index a5365c237..1cae6b23e 100644
--- a/guake/boxes.py
+++ b/guake/boxes.py
@@ -1,3 +1,4 @@
+import enum
import logging
import time
@@ -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
@@ -637,9 +646,9 @@ 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):
@@ -647,7 +656,7 @@ 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(
@@ -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''
f"{GLib.markup_escape_text(self._text)}"
)
+ elif self._activity_state == TabActivityState.STALE:
+ self.label.set_markup(
+ f''
+ f"{GLib.markup_escape_text(self._text)}"
+ )
else:
self.label.set_text(self._text)
diff --git a/guake/data/org.guake.gschema.xml b/guake/data/org.guake.gschema.xml
index cad5252fa..5fdc555c3 100644
--- a/guake/data/org.guake.gschema.xml
+++ b/guake/data/org.guake.gschema.xml
@@ -176,16 +176,6 @@
Highlight tabs with activityIf true, the title of a background tab is highlighted when its terminal produces output, until the tab is selected.
-
- 3.0
- Activity grace period for new tabs
- Number of seconds during which activity is ignored on a newly created tab, to avoid highlighting it because of its initial shell prompt.
-
-
- 1.0
- Activity grace period after losing focus
- Number of seconds during which activity is ignored on a tab right after it loses focus.
- 100Window width.
diff --git a/guake/data/prefs.glade b/guake/data/prefs.glade
index 1e31db6a2..212a8ee8e 100644
--- a/guake/data/prefs.glade
+++ b/guake/data/prefs.glade
@@ -423,90 +423,6 @@
5
-
-
-
- 0
- 6
- 2
-
-
-
-
- True
- False
- 6
-
-
- True
- False
- center
- Ignore activity after losing focus for (seconds):
-
-
- False
- False
- 0
-
-
-
-
- True
- True
- 1.0
- tab_activity_focus_loss_grace_adj
- 1
- 1
- True
-
-
-
- False
- True
- 1
-
-
-
-
- 0
- 7
- 2
-
- True
diff --git a/guake/notebook.py b/guake/notebook.py
index 96e199b65..dc78ff329 100644
--- a/guake/notebook.py
+++ b/guake/notebook.py
@@ -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
@@ -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
@@ -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):
@@ -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:
@@ -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
)
@@ -450,9 +465,34 @@ 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
@@ -460,24 +500,23 @@ def on_switch_page(self, notebook, page, page_num):
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)
diff --git a/guake/prefs.py b/guake/prefs.py
index 42c240625..3d092daba 100644
--- a/guake/prefs.py
+++ b/guake/prefs.py
@@ -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):
@@ -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")
diff --git a/releasenotes/notes/tab_activity_cooldown-5cf02db0a9a035dd.yaml b/releasenotes/notes/tab_activity_cooldown-5cf02db0a9a035dd.yaml
new file mode 100644
index 000000000..4597d698e
--- /dev/null
+++ b/releasenotes/notes/tab_activity_cooldown-5cf02db0a9a035dd.yaml
@@ -0,0 +1,9 @@
+release_summary: >
+ 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
+
diff --git a/releasenotes/notes/tab_activity_indication-be54b262d370a96b.yaml b/releasenotes/notes/tab_activity_indication-be54b262d370a96b.yaml
index c12dcc9fe..aa19d4a5a 100644
--- a/releasenotes/notes/tab_activity_indication-be54b262d370a96b.yaml
+++ b/releasenotes/notes/tab_activity_indication-be54b262d370a96b.yaml
@@ -6,18 +6,3 @@ features:
- |
- add tabs activity indication
-known_issues:
-
-upgrade:
-
-deprecations:
-
-security:
-
-fixes:
-
-translations:
-
-notes_for_package_maintainers:
-
-other: