From c3f22f0794b5a3e5e5c2ff30d11f8b6d4542c586 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sun, 26 Jul 2026 12:54:23 +0700 Subject: [PATCH] Stop the indicator from making the window behind it flinch Hiding the indicator unmapped its window, so KWin tore that window down and built a new one for the next dictation. On a tiled desktop the compositor repainted whatever sat underneath as it went, and the terminal behind visibly flinched every time a transcript landed. Measured with a KWin script: five show-and-hide rounds raised five windowAdded and five windowRemoved events, where there is now one of each for the life of the process. So the window stays mapped and paints nothing while idle. That has to be a real repaint rather than zero opacity: with the animation timer stopped nothing else damages the surface, and the stale frame sat on the screen until something unrelated, moving the mouse, made the compositor redraw it. The timer was also left running after the indicator went away, ticking thirty times a second over a window nobody could see. It stops now, and the finished message lingers for two seconds rather than one. --- overlay.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/overlay.py b/overlay.py index 0fd2512..da55138 100644 --- a/overlay.py +++ b/overlay.py @@ -35,6 +35,7 @@ class Overlay(QWidget): self.levels = [0.0] * BARS self.seconds = 0.0 self._phase = 0.0 + self._concealed = True self.setWindowFlags( Qt.WindowType.FramelessWindowHint @@ -55,7 +56,7 @@ class Overlay(QWidget): self._hide_timer = QTimer(self) self._hide_timer.setSingleShot(True) - self._hide_timer.timeout.connect(self.hide) + self._hide_timer.timeout.connect(self._conceal) # ---- public API -------------------------------------------------- @@ -73,7 +74,7 @@ class Overlay(QWidget): self._hide_timer.stop() self._appear() - def show_done(self, message="", msec=1100): + def show_done(self, message="", msec=2000): self.state = "done" self.message = message self._appear() @@ -93,9 +94,8 @@ class Overlay(QWidget): self._hide_timer.start(msec) def dismiss(self): - self._anim.stop() self._hide_timer.stop() - self.hide() + self._conceal() def push_level(self, level): self.levels = self.levels[1:] + [level] @@ -110,10 +110,28 @@ class Overlay(QWidget): self._reposition() if not self.isVisible(): self.show() - self.raise_() + if self._concealed: + self.raise_() + self._concealed = False if not self._anim.isActive(): self._anim.start() + def _conceal(self): + """Empty the window out instead of unmapping it. + + Unmapping tears the window down and the next dictation builds a new one, + which makes the compositor repaint whatever sits underneath: on a tiled + desktop the terminal behind visibly flinches every time the indicator + goes away. So the window stays mapped and simply paints nothing. It has + to be a real repaint, not just zero opacity: with the animation stopped + nothing else damages the surface, and the stale frame would sit on the + screen until some other event made the compositor redraw it. + """ + self._anim.stop() + self.state = "hidden" + self._concealed = True + self.repaint() + def _resize_to_content(self): if self.state == "recording": width = MIN_WIDTH @@ -147,6 +165,9 @@ class Overlay(QWidget): # ---- painting -------------------------------------------------- def paintEvent(self, _event): + if self.state == "hidden": + return # translucent window, nothing drawn means nothing shown + painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) rect = QRectF(0.5, 0.5, self.width() - 1, self.height() - 1)