Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notification actions #350

Merged
merged 3 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading