Skip to content

Commit

Permalink
feat: Rendering notification actions and triggered source (nightly) #350
Browse files Browse the repository at this point in the history


from tomlin7/notification-actions
  • Loading branch information
tomlin7 authored Jun 19, 2024
2 parents 476b157 + 2a29d8e commit 88d3c6c
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 63 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "biscuit"
version = "2.97.0"
version = "2.98.0"
description = "The uncompromising code editor"
authors = ["Billy <billydevbusiness@gmail.com>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.97.0"
__version__ = "2.98.0"
__version_info__ = tuple([int(num) for num in __version__.split(".")])

import sys
Expand Down
4 changes: 4 additions & 0 deletions src/biscuit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def __init__(self, appdir: str = "", dir: str = "", *args, **kwargs) -> None:
self.late_setup()
self.initialize_app(dir)

self.notifications.info(
"Welcome to Biscuit!", [("Close", lambda: print("Closed"))]
)

def run(self) -> None:
"""Start the main loop of the app."""

Expand Down
26 changes: 26 additions & 0 deletions src/biscuit/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,29 @@ def caller_name(skip=2):
del parentframe, stack

return ".".join(name)


def caller_class_name(skip=2):
"""
Get the name of the class of the caller.
`skip` specifies how many levels of stack to skip while getting the caller's class.
skip=1 means "who calls me", skip=2 "who calls my caller" etc.
An empty string is returned if skipped levels exceed the stack height.
"""
stack = inspect.stack()
start = 0 + skip
if len(stack) < start + 1:
return ""

parentframe = stack[start][0]
class_name = None

# detect classname
if "self" in parentframe.f_locals:
class_name = parentframe.f_locals["self"].__class__.__name__

del parentframe, stack

return class_name
57 changes: 40 additions & 17 deletions src/biscuit/common/notifications/manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import tkinter as tk
from typing import Callable

from numpy import pad

from ..ui import Frame, IconButton, Label, Toplevel
from .notification import Notification
Expand Down Expand Up @@ -60,57 +63,72 @@ def __init__(self, base) -> None:

close_button = IconButton(topbar, "chevron-down", self.hide)
close_button.config(**self.base.theme.notifications.title)
close_button.pack(side=tk.RIGHT, fill=tk.BOTH)
close_button.pack(side=tk.RIGHT, fill=tk.BOTH, pady=(0, 1))

self.base.bind("<FocusIn>", lambda *_: self.lift, add=True)
self.base.bind("<Configure>", self._follow_root, add=True)

def info(self, text: str) -> Notification:
def info(
self, text: str, actions: list[tuple[str, Callable[[None], None]]] = None
) -> Notification:
"""Create an info notification
Args:
text (str): notification text"""

instance = Notification(self, "info", text=text, fg=self.base.theme.biscuit)
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
instance = Notification(
self, "info", text=text, fg=self.base.theme.biscuit, actions=actions
)
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1, pady=(0, 1))
self.count += 1
self.show()

self.notifications.append(instance)
self.latest = instance
return instance

def warning(self, text: str) -> Notification:
def warning(
self, text: str, actions: list[tuple[str, Callable[[None], None]]] = None
) -> Notification:
"""Create a warning notification
Args:
text (str): notification text"""

instance = Notification(self, "warning", text=text, fg="yellow")
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
instance = Notification(
self, "warning", text=text, fg="yellow", actions=actions
)
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1, pady=(0, 1))
self.count += 1
self.show()

self.notifications.append(instance)
self.latest = instance
return instance

def error(self, text: str) -> Notification:
def error(
self, text: str, actions: list[tuple[str, Callable[[None], None]]] = None
) -> Notification:
"""Create an error notification
Args:
text (str): notification text"""

instance = Notification(self, "error", text=text, fg="red")
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
instance = Notification(self, "error", text=text, fg="red", actions=actions)
instance.pack(side=tk.TOP, fill=tk.BOTH, expand=1, pady=(0, 1))
self.count += 1
self.show()

self.notifications.append(instance)
self.latest = instance
return instance

def notify(self, text: str, kind: int) -> Notification:
def notify(
self,
text: str,
kind: int,
actions: list[tuple[str, Callable[[None], None]]] = None,
) -> Notification:
"""Create a notification based on kind
1: info
2: warning
Expand All @@ -122,11 +140,11 @@ def notify(self, text: str, kind: int) -> Notification:
"""
match kind:
case 1:
self.error(text)
self.error(text, actions)
case 2:
self.warning(text)
self.warning(text, actions)
case _:
self.info(text)
self.info(text, actions)

def _follow_root(self, *_) -> None:
"""Follow root window position"""
Expand All @@ -153,6 +171,14 @@ def _follow_root(self, *_) -> None:
# root window is destroyed
pass

def toggle(self, *_) -> None:
"""Toggle notification visibility"""

if self.active:
self.hide()
else:
self.show()

def show(self, *_) -> None:
"""Toggle notification visibility
Also updates title based on count."""
Expand All @@ -162,9 +188,6 @@ def show(self, *_) -> None:
else:
self.title.config(text="NO NEW NOTIFICATIONS")

if self.active:
return self.hide()

self.active = True
self.deiconify()
self._follow_root()
Expand Down
48 changes: 41 additions & 7 deletions src/biscuit/common/notifications/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import tkinter as tk
import typing

from ..ui import Frame, Icon, IconButton, Label
from ..helpers import caller_class_name
from ..ui import Frame, Icon, IconButton, IconLabelButton, Label

if typing.TYPE_CHECKING:
from .manager import Notifications
Expand All @@ -15,26 +16,39 @@ class Notification(Frame):
Holds the notification icon, text and close button"""

def __init__(
self, master: Notifications, icon: str, text: str, fg: str, *args, **kwargs
self,
master: Notifications,
icon: str,
text: str,
actions: list[tuple[str, typing.Callable[[None], None]]],
fg: str,
*args,
**kwargs,
) -> None:
"""Create a notification
Args:
master: Parent widget
icon (str): Icon name
text (str): Notification text
fg (str): Foreground color"""
fg (str): Foreground color
actions (list[tuple(str, Callable[[None], None])]): List of actions"""

super().__init__(master, *args, **kwargs)
self.master: Notifications = master
self.config(**self.base.theme.notifications)

self.icon = Icon(self, icon, padx=5, **self.base.theme.utils.iconbutton)
top = Frame(self, **self.base.theme.utils.frame)
top.pack(fill=tk.BOTH, expand=1)
bottom = Frame(self, **self.base.theme.utils.frame)
bottom.pack(fill=tk.BOTH)

self.icon = Icon(top, icon, padx=5, **self.base.theme.utils.iconbutton)
self.icon.config(fg=fg)
self.icon.pack(side=tk.LEFT, fill=tk.BOTH)

self.label = Label(
self,
top,
text=text,
anchor=tk.W,
padx=10,
Expand All @@ -43,8 +57,28 @@ def __init__(
)
self.label.pack(side=tk.LEFT, expand=1, fill=tk.BOTH)

close_button = IconButton(self, "close", self.delete)
close_button.pack(side=tk.RIGHT, fill=tk.BOTH)
close_button = IconButton(top, "close", self.delete)
close_button.pack(fill=tk.BOTH)

self.source_label = Label(
bottom,
text=f"Source: {caller_class_name(3)}",
anchor=tk.W,
padx=10,
pady=5,
**self.base.theme.notifications.source,
)
self.source_label.pack(side=tk.LEFT, fill=tk.BOTH)

if actions:
self.actions = Frame(bottom, **self.base.theme.notifications)
self.actions.pack(side=tk.RIGHT, fill=tk.BOTH)

for text, action in actions:
btn = IconLabelButton(
self.actions, text, callback=action, highlighted=True
)
btn.pack(side=tk.LEFT, fill=tk.BOTH, padx=(0, 5), pady=(0, 5))

def delete(self):
"""Delete the notification"""
Expand Down
4 changes: 3 additions & 1 deletion src/biscuit/editor/text/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ def unsaved_changes(self):

def run_file(self, dedicated=False, external=False):
if not self.run_command_value:
self.base.notifications.show("No programs are configured to run this file.")
self.base.notifications.warning(
"No programs are configured to run this file."
)
self.base.commands.show_run_config_palette(self.run_command_value)
return

Expand Down
8 changes: 8 additions & 0 deletions src/biscuit/git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def late_setup(self) -> None:
"""This function is called after the app is initialized."""

if not git_available:
self.base.notifications.warning(
"Git not found in PATH, git features are disabled"
)
self.base.logger.warning("Git not found in PATH, git features are disabled")
return

self.base.palette.register_actionset(lambda: self.actionset)
Expand All @@ -70,6 +74,8 @@ def check_git(self) -> None:
try:
self.repo = GitRepo(self, self.base.active_directory)
self.base.git_found = True
self.base.notifications.info("Git repository found in opened directory")
self.base.logger.info("Git repository found in opened directory")
self.update_repo_info()
except git.exc.InvalidGitRepositoryError:
self.base.git_found = False
Expand Down Expand Up @@ -115,6 +121,8 @@ def checkout(self, branch: str) -> None:
return

self.repo.index.checkout(branch)
self.base.notifications.info(f"Checked out branch {branch}")
self.base.logger.info(f"Checked out branch {branch}")

def clone(self, url: str, dir: str) -> str:
"""Clone a git repository to the specified directory
Expand Down
2 changes: 1 addition & 1 deletion src/biscuit/layout/statusbar/statusbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __init__(self, master: Frame, *args, **kwargs) -> None:
# ---------------------------------------------------------------------
self.notif = self.add_button(
icon="bell",
callback=self.base.notifications.show,
callback=self.base.notifications.toggle,
description="No notifications",
side=tk.RIGHT,
padx=(0, 10),
Expand Down
24 changes: 20 additions & 4 deletions src/biscuit/settings/theme/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ class DrawerPane(FrameThemeObject):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.actionbar = FrameThemeObject(self)
self.actionbar.slot = HighlightableThemeObject(self.actionbar).remove_bg_highlight()
self.actionbar.slot = HighlightableThemeObject(
self.actionbar
).remove_bg_highlight()
self.actionbar.bubble = ThemeObject(self.actionbar)


Expand Down Expand Up @@ -216,11 +218,17 @@ def __init__(self, *args, **kwargs) -> None:


class Notifications(FrameThemeObject):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.title = ThemeObject(self)
def __init__(self, master: Theme, *args, **kwargs) -> None:
super().__init__(master, *args, **kwargs)
self.theme = master
self.title = ThemeObject(self, fg=master.secondary_foreground)
self.button = HighlightableThemeObject(self)
self.text = ThemeObject(self)
self.source = ThemeObject(
self,
master.primary_background,
master.primary_foreground,
)


class Editors(FrameThemeObject):
Expand Down Expand Up @@ -303,6 +311,7 @@ def __init__(self, *args, **kwargs) -> None:
theme.secondary_foreground,
self.highlightbackground,
)
self.frame = FrameThemeObject(self)
self.buttonsentry = ThemeObject(
self,
theme.secondary_background,
Expand Down Expand Up @@ -361,6 +370,13 @@ def __init__(self) -> None:
self.secondary_foreground_highlight,
]

self.foreground = self.primary_foreground
self.background = self.primary_background
self.highlightbackground = self.primary_background_highlight
self.highlightforeground = self.primary_foreground_highlight
self.selectedbackground = self.primary_background_highlight
self.selectedforeground = self.primary_foreground_highlight

self.layout = Layout(self, *primary)
self.views = Views(self, *primary)
self.utils = Utils(self, *primary)
Expand Down
Loading

0 comments on commit 88d3c6c

Please sign in to comment.