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 activity If 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. - 100 Window 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 - - - True - False - 6 - - - True - False - center - Ignore activity on new tabs for (seconds): - - - False - False - 0 - - - - - True - True - 3.0 - tab_activity_new_tab_grace_adj - 1 - 1 - True - - - - False - True - 1 - - - - - 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: