Skip to content

Commit

Permalink
Add animation
Browse files Browse the repository at this point in the history
  • Loading branch information
hluk committed Nov 18, 2023
1 parent b136b3d commit 4fb6d0e
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 48 deletions.
81 changes: 81 additions & 0 deletions xitomatl/animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from PySide6.QtCore import (
QEasingCurve,
QObject,
Qt,
QTimer,
QVariantAnimation,
Signal,
)
from PySide6.QtGui import QIcon, QPixmap, QTransform


class NotifyAnimation(QObject):
icon_changed = Signal(QIcon)

def __init__(self):
super().__init__()
self.value = 0.0

self.curve1 = QEasingCurve(QEasingCurve.Type.InOutQuad)
self.anim1 = QVariantAnimation()
self.anim1.setEasingCurve(self.curve1)
self.anim1.setStartValue(self.value)
self.anim1.setEndValue(-15.0)
self.anim1.setDuration(250)
self.anim1.valueChanged.connect(self.set_value)

self.curve2 = QEasingCurve(QEasingCurve.Type.OutElastic)
self.curve2.setAmplitude(3)
self.curve2.setPeriod(0.15)
self.anim2 = QVariantAnimation()
self.anim2.setEasingCurve(self.curve2)
self.anim2.setStartValue(self.anim1.endValue())
self.anim2.setEndValue(self.value)
self.anim2.setDuration(1500)
self.anim2.valueChanged.connect(self.set_value)

self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.setInterval(10000)

self.anim1.finished.connect(self.anim2.start)
self.anim2.finished.connect(self.timer.start)
self.timer.timeout.connect(self.start)

self.icon = QPixmap()

@property
def interval(self):
return self.timer.interval()

@interval.setter
def interval(self, milliseconds):
self.timer.setInterval(milliseconds)

def start(self):
if self.anim1.state() == QVariantAnimation.State.Running:
return

if self.anim2.state() == QVariantAnimation.State.Running:
return

self.anim1.start()

def stop(self):
self.anim1.stop()

def set_icon(self, icon):
self.icon = icon
self.update_icon()

def set_value(self, value):
self.value = value
self.update_icon()

def update_icon(self):
transform = QTransform()
transform.rotate(self.value)
icon = self.icon.transformed(
transform, Qt.TransformationMode.SmoothTransformation
)
self.icon_changed.emit(QIcon(icon))
18 changes: 15 additions & 3 deletions xitomatl/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon

from xitomatl.animation import NotifyAnimation
from xitomatl.icon import task_icon
from xitomatl.pomodoro import Pomodoro, State

Expand Down Expand Up @@ -36,6 +37,9 @@ def __init__(self, argv, settings):

self.icon_size = int(settings.value("icon_size", DEFAULT_ICON_SIZE))

self.animation = NotifyAnimation()
self.animation.icon_changed.connect(self.icon.setIcon)

menu = QMenu()
menu.addAction(
QIcon.fromTheme("media-playback-start"),
Expand Down Expand Up @@ -85,13 +89,21 @@ def on_icon_middle_click(self):
self.pomodoro.stop()

def on_state_changed(self):
task = self.pomodoro.current_task()
remaining = self.pomodoro.remaining_minutes()
icon = task_icon(
self.pomodoro.current_task(),
task,
self.pomodoro.state,
self.pomodoro.remaining_minutes(),
remaining,
self.icon_size,
)
self.icon.setIcon(icon)

self.animation.set_icon(icon)
if task.animated and remaining <= 0:
self.animation.start()
else:
self.animation.stop()

self.icon.setToolTip(f"{QApplication.applicationName()}: {self.pomodoro}")

act = self.task_actions[self.current_task_index]
Expand Down
5 changes: 2 additions & 3 deletions xitomatl/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from PySide6.QtGui import (
QFont,
QFontMetrics,
QIcon,
QPainter,
QPainterPath,
QPen,
Expand Down Expand Up @@ -48,7 +47,7 @@ def render_text(painter, task, size, icon_text):


def task_icon(task, state, remaining_minutes, icon_size):
"""Created QIcon for given task and state."""
"""Created QPixmap for given task and state."""
pix = QPixmap(icon_size, icon_size)
pix.fill(Qt.transparent)
try:
Expand Down Expand Up @@ -88,4 +87,4 @@ def task_icon(task, state, remaining_minutes, icon_size):
finally:
painter.end()

return QIcon(pix)
return pix
91 changes: 49 additions & 42 deletions xitomatl/tasks.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
# SPDX-License-Identifier: LGPL-2.0-or-later
from copy import copy
from dataclasses import dataclass, field, fields

from PySide6.QtGui import QColor

DEFAULT_FONT = "Noto Sans Mono Condensed"
DEFAULT_TIMEOUT_FONT = "Noto Sans Mono Condensed ExtraBold"
DEFAULT_TASK_CACHE_KEY = "__default__"
DEFAULT_TASK_ARGS = dict(
name="focus",
minutes=25,
in_menu=True,
command_start="",
command_stop="",
command_finish="",
image="",
# Normal appearance options
font=DEFAULT_FONT,
color=QColor("#007ba7"),
line_color=QColor("transparent"),
line_width=0,
text_color=QColor("white"),
text_stroke_width=0,
text_stroke_color=QColor("transparent"),
text_size=65,
text_x=0,
text_y=0,
icon_radius=30,
icon_padding=10,
# Timed out appearance options
timeout_font=DEFAULT_TIMEOUT_FONT,
timeout_color=QColor("#ff0040"),
timeout_line_color=QColor("transparent"),
timeout_line_width=0,
timeout_text_color=QColor("white"),
timeout_text_stroke_width=0,
timeout_text_stroke_color=QColor("transparent"),
timeout_text_size=65,
timeout_text_x=0,
timeout_text_y=0,
timeout_icon_radius=30,
timeout_icon_padding=10,
)


def to_bool(value):
Expand All @@ -61,10 +27,50 @@ def __getattr__(self, attr):
return getattr(self.task, attr)


def color_field(color_name):
# pylint: disable=invalid-field-call
return field(default_factory=lambda: QColor(color_name))


@dataclass
class Task:
def __init__(self, **kwargs):
for k, v in DEFAULT_TASK_ARGS.items():
setattr(self, k, kwargs.get(k, v))
name: str = "focus"
minutes: int = 25
in_menu: bool = True
command_start: str = ""
command_stop: str = ""
command_finish: str = ""
image: str = ""

# Normal appearance options
font: str = DEFAULT_FONT
color: QColor = color_field("#007ba7")
line_color: QColor = color_field("transparent")
line_width: int = 0
text_color: QColor = color_field("white")
text_stroke_width: int = 0
text_stroke_color: QColor = color_field("transparent")
text_size: int = 65
text_x: int = 0
text_y: int = 0
icon_radius: int = 30
icon_padding: int = 10

# Timed out appearance options
timeout_font = DEFAULT_TIMEOUT_FONT
timeout_color: QColor = color_field("#ff0040")
timeout_line_color: QColor = color_field("transparent")
timeout_line_width: int = 0
timeout_text_color: QColor = color_field("white")
timeout_text_stroke_width: int = 0
timeout_text_stroke_color: QColor = color_field("transparent")
timeout_text_size: int = 65
timeout_text_x: int = 0
timeout_text_y: int = 0
timeout_icon_radius: int = 30
timeout_icon_padding: int = 10

animated: bool = True

def __str__(self):
return f"{self.name}/{self.minutes}"
Expand All @@ -85,6 +91,7 @@ def __init__(self, minutes=5, in_menu=False):
in_menu=in_menu,
icon_radius=100,
text_y=0,
animated=True,
)


Expand All @@ -94,13 +101,13 @@ def read_task(settings, task_cache):
default_task.name = name
task = task_cache.setdefault(name, default_task)

for key, default_value in DEFAULT_TASK_ARGS.items():
value = settings.value(key)
for field_ in fields(Task):
value = settings.value(field_.name)
if value:
convert = type(default_value)
convert = field_.type
if convert is bool:
convert = to_bool
setattr(task, key, convert(value))
setattr(task, field_.name, convert(value))

task_cache[DEFAULT_TASK_CACHE_KEY] = copy(task)
return copy(task)
Expand Down

0 comments on commit 4fb6d0e

Please sign in to comment.