From 323efa11edbe6cbf853d45ccf29ac609dd3fcd7b Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 19 Jun 2024 20:55:08 +0900 Subject: [PATCH 1/3] feat: Notifications can take actions and also show the source it's triggered from --- src/biscuit/app.py | 4 ++ src/biscuit/common/helpers.py | 26 +++++++++ src/biscuit/common/notifications/manager.py | 57 +++++++++++++------ .../common/notifications/notification.py | 46 +++++++++++++-- src/biscuit/editor/text/editor.py | 4 +- src/biscuit/git/git.py | 8 +++ src/biscuit/layout/statusbar/statusbar.py | 2 +- src/biscuit/settings/theme/theme.py | 24 ++++++-- src/biscuit/views/logs/logs.py | 38 +++---------- src/biscuit/views/terminal/terminal.py | 2 +- 10 files changed, 151 insertions(+), 60 deletions(-) diff --git a/src/biscuit/app.py b/src/biscuit/app.py index b637b0c1..5a255956 100644 --- a/src/biscuit/app.py +++ b/src/biscuit/app.py @@ -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.""" diff --git a/src/biscuit/common/helpers.py b/src/biscuit/common/helpers.py index 120ef4d3..d4e87547 100644 --- a/src/biscuit/common/helpers.py +++ b/src/biscuit/common/helpers.py @@ -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 diff --git a/src/biscuit/common/notifications/manager.py b/src/biscuit/common/notifications/manager.py index 6fb195cc..46c848f9 100644 --- a/src/biscuit/common/notifications/manager.py +++ b/src/biscuit/common/notifications/manager.py @@ -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 @@ -60,19 +63,23 @@ 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("", lambda *_: self.lift, add=True) self.base.bind("", 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() @@ -80,14 +87,18 @@ def info(self, text: str) -> Notification: 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() @@ -95,14 +106,16 @@ def warning(self, text: str) -> Notification: 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() @@ -110,7 +123,12 @@ def error(self, text: str) -> Notification: 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 @@ -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""" @@ -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.""" @@ -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() diff --git a/src/biscuit/common/notifications/notification.py b/src/biscuit/common/notifications/notification.py index 055375ad..2ef87f00 100644 --- a/src/biscuit/common/notifications/notification.py +++ b/src/biscuit/common/notifications/notification.py @@ -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 @@ -15,7 +16,14 @@ 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 @@ -23,7 +31,8 @@ def __init__( 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 @@ -33,8 +42,13 @@ def __init__( self.icon.config(fg=fg) self.icon.pack(side=tk.LEFT, fill=tk.BOTH) + 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.label = Label( - self, + top, text=text, anchor=tk.W, padx=10, @@ -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""" diff --git a/src/biscuit/editor/text/editor.py b/src/biscuit/editor/text/editor.py index f5d248ca..cd5a32e9 100644 --- a/src/biscuit/editor/text/editor.py +++ b/src/biscuit/editor/text/editor.py @@ -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 diff --git a/src/biscuit/git/git.py b/src/biscuit/git/git.py index 28e103bc..6872d5a4 100644 --- a/src/biscuit/git/git.py +++ b/src/biscuit/git/git.py @@ -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) @@ -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 @@ -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 diff --git a/src/biscuit/layout/statusbar/statusbar.py b/src/biscuit/layout/statusbar/statusbar.py index 9960e9a2..76dda1de 100644 --- a/src/biscuit/layout/statusbar/statusbar.py +++ b/src/biscuit/layout/statusbar/statusbar.py @@ -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), diff --git a/src/biscuit/settings/theme/theme.py b/src/biscuit/settings/theme/theme.py index dc1e2481..d8c99fb8 100644 --- a/src/biscuit/settings/theme/theme.py +++ b/src/biscuit/settings/theme/theme.py @@ -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) @@ -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): @@ -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, @@ -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) diff --git a/src/biscuit/views/logs/logs.py b/src/biscuit/views/logs/logs.py index 287ac6ca..4b913857 100644 --- a/src/biscuit/views/logs/logs.py +++ b/src/biscuit/views/logs/logs.py @@ -3,37 +3,12 @@ import tkinter as tk from datetime import datetime +from src.biscuit.common import caller_class_name from src.biscuit.common.ui import Scrollbar from ..panelview import PanelView -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 - - class Logs(PanelView): """The Logs view. @@ -120,22 +95,25 @@ def log(self, type: str, caller: str, text: str) -> None: def info(self, text: str) -> None: """info level log""" - self.log((" [info] ", "info"), caller_class_name(), text) + self._std_log(" [info] ", "info") def warning(self, text: str) -> None: """warning level log""" - self.log((" [warning] ", "warning"), caller_class_name(), text) + self._std_log(" [warning] ", "warning") def error(self, text: str) -> None: """error level log""" - self.log((" [error] ", "error"), caller_class_name(), text) + self._std_log(" [error] ", "error") def trace(self, text: str) -> None: """trace level log""" - self.log((" [trace] ", "trace"), caller_class_name(), text) + self._std_log(" [trace] ", "trace") + + def _std_log(self, text: str, kind: int) -> None: + self.log((text, kind), caller_class_name(), text) def rawlog(self, text: str, kind: int): match kind: diff --git a/src/biscuit/views/terminal/terminal.py b/src/biscuit/views/terminal/terminal.py index dc31970b..1e3a05e2 100644 --- a/src/biscuit/views/terminal/terminal.py +++ b/src/biscuit/views/terminal/terminal.py @@ -146,7 +146,7 @@ def run_external_console(self, command: str) -> None: case "Darwin": subprocess.Popen(["open", "-a", "Terminal", command]) case _: - self.base.notifications.show("No terminal emulator detected.") + self.base.notifications.warning("No terminal emulator detected.") # TODO: Implement these def open_pwsh(self): ... From 3eb76d14ab0b597fe1cdc4db60938bb16777cea3 Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 19 Jun 2024 20:56:32 +0900 Subject: [PATCH 2/3] fix: Notification icons are shown in top container --- src/biscuit/common/notifications/notification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/biscuit/common/notifications/notification.py b/src/biscuit/common/notifications/notification.py index 2ef87f00..bf1e21c2 100644 --- a/src/biscuit/common/notifications/notification.py +++ b/src/biscuit/common/notifications/notification.py @@ -38,15 +38,15 @@ def __init__( self.master: Notifications = master self.config(**self.base.theme.notifications) - self.icon = Icon(self, icon, padx=5, **self.base.theme.utils.iconbutton) - self.icon.config(fg=fg) - self.icon.pack(side=tk.LEFT, fill=tk.BOTH) - 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( top, text=text, From 2a29d8ec01b0666bc458122c9f906b4d5aba8313 Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 19 Jun 2024 20:57:44 +0900 Subject: [PATCH 3/3] chore: Bump version to `2.98.0` --- pyproject.toml | 2 +- src/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0a80e6b7..e2dba1f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "biscuit" -version = "2.97.0" +version = "2.98.0" description = "The uncompromising code editor" authors = ["Billy "] license = "MIT" diff --git a/src/__init__.py b/src/__init__.py index bd77cc4d..798b3bc2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.97.0" +__version__ = "2.98.0" __version_info__ = tuple([int(num) for num in __version__.split(".")]) import sys