from threading import Thread from src.backend.PluginManager.ActionBase import ActionBase from src.backend.DeckManagement.DeckController import DeckController from src.backend.PageManagement.Page import Page from src.backend.PluginManager.PluginBase import PluginBase import matplotlib.pyplot as plt import matplotlib from multiprocessing import Process, Queue # Use different backend to prevent errors with running plt in different threads matplotlib.use('agg') from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from PIL import Image import io # Import gtk import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gdk # Import globals import globals as gl from src.Signals import Signals class GraphBase(ActionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.percentages: list[float] = [] self.task_queue = Queue() self.result_queue = Queue() # self.process = Process(target=self.worker, args=(self.task_queue, self.result_queue), name="GraphBaseCreator") self.process = GraphCreator(task_queue=self.task_queue, result_queue=self.result_queue) self.process.start() gl.signal_manager.connect_signal(Signals.AppQuit, self.stop_process) def stop_process(self, *args): print("stop graph base") self.task_queue.put((None, None)) def set_percentages_lenght(self, length: int): if len(self.percentages) > length: self.percentages = self.percentages[-length:] elif len(self.percentages) < length: for _ in range(length - len(self.percentages)): self.percentages.insert(0, 0) return self.percentages def get_graph(self) -> Image: ## Get vars settings = self.get_settings() time_period = settings.get("time-period", 15) self.set_percentages_lenght(time_period) self.task_queue.put((settings, self.percentages)) img = self.result_queue.get() return img def show_graph(self): image = self.get_graph() if image is None: return self.set_media(image=image) def get_custom_config_area(self): return Gtk.Label(label=self.plugin_base.lm.get("actions.graph-base.memory-warning"), css_classes=["destructive-action"]) def get_config_rows(self) -> list: self.line_color_row = ColorRow() self.line_color_row.color_label.set_label("Line Color:") self.fill_color = ColorRow() self.fill_color.color_label.set_label("Fill Color:") self.line_width_row = Adw.SpinRow.new_with_range(1, 30, 1) self.line_width_row.set_title("Line Width:") self.time_period_row = Adw.SpinRow.new_with_range(1, 60, 1) self.time_period_row.set_title("Time Period (s):") self.dynamic_scaling_row = Adw.SwitchRow(title="Dynamic Y-axis Scaling:") self.toggle_dynamic_scaling_on_press_row = Adw.SwitchRow(title="Toggle Dynamic Y-axis Scaling on Press:") ## Load defaults: settings = self.get_settings() self.fill_color.color_button.set_rgba(self.prepare_color(settings.get("fill-color", [255, 255, 255, 255]))) self.line_color_row.color_button.set_rgba(self.prepare_color(settings.get("line-color", [255, 255, 255, 150]))) self.line_width_row.set_value(settings.get("line-width", 5)) self.time_period_row.set_value(settings.get("time-period", 15)) self.dynamic_scaling_row.set_active(settings.get("dynamic-scaling", False)) self.toggle_dynamic_scaling_on_press_row.set_active(settings.get("toggle-dynamic-scaling-on-press", False)) ## Connect signals self.fill_color.color_button.connect("color-set", self.on_fill_color_change) self.line_color_row.color_button.connect("color-set", self.on_line_color_change) self.line_width_row.connect("changed", self.on_line_width_change) self.time_period_row.connect("changed", self.on_time_period_change) self.dynamic_scaling_row.connect("notify::active", self.on_dynamic_scaling_change) self.toggle_dynamic_scaling_on_press_row.connect("notify::active", self.on_dynamic_scaling_toggle_on_press_change) return [self.line_color_row, self.fill_color, self.line_width_row, self.time_period_row, self.dynamic_scaling_row, self.toggle_dynamic_scaling_on_press_row] def prepare_color(self, color_values: list[int]) -> Gdk.RGBA: if len(color_values) == 3: color_values.append(255) color = Gdk.RGBA() color.parse(f"rgba({color_values[0]}, {color_values[1]}, {color_values[2]}, {color_values[3]})") return color def on_fill_color_change(self, button): color = self.fill_color.color_button.get_rgba() green = round(color.green * 255) blue = round(color.blue * 255) red = round(color.red * 255) alpha = round(color.alpha * 255) settings = self.get_settings() settings["fill-color"] = [red, green, blue, alpha] self.set_settings(settings) self.show_graph() def on_line_color_change(self, button): color = self.line_color_row.color_button.get_rgba() green = round(color.green * 255) blue = round(color.blue * 255) red = round(color.red * 255) alpha = round(color.alpha * 255) settings = self.get_settings() settings["line-color"] = [red, green, blue, alpha] self.set_settings(settings) self.show_graph() def on_line_width_change(self, spin): settings = self.get_settings() settings["line-width"] = spin.get_value() self.set_settings(settings) self.show_graph() def on_time_period_change(self, spin): settings = self.get_settings() settings["time-period"] = int(spin.get_value()) self.set_settings(settings) self.set_percentages_lenght(int(spin.get_value())) self.show_graph() def on_dynamic_scaling_change(self, switch, *args): settings = self.get_settings() settings["dynamic-scaling"] = switch.get_active() self.set_settings(settings) self.show_graph() def on_dynamic_scaling_toggle_on_press_change(self, switch, *args): settings = self.get_settings() settings["toggle-dynamic-scaling-on-press"] = switch.get_active() self.set_settings(settings) self.show_graph() def on_key_down(self): settings = self.get_settings() if not settings.get("toggle-dynamic-scaling-on-press", False): return settings["dynamic-scaling"] = not settings.get("dynamic-scaling", False) self.set_settings(settings) if hasattr(self, "dynamic_scaling_row"): self.dynamic_scaling_row.set_active(settings["dynamic-scaling"]) self.show_graph() def on_removed_from_cache(self) -> None: self.task_queue.put((None, None)) class ColorRow(Adw.PreferencesRow): def __init__(self): super().__init__() self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True, margin_top=10, margin_bottom=10, margin_start=15, margin_end=15) self.set_child(self.main_box) self.color_label = Gtk.Label(label="Color:", hexpand=True, xalign=0) self.main_box.append(self.color_label) self.color_button = Gtk.ColorButton(use_alpha=True) self.main_box.append(self.color_button) class GraphCreator(Process): def __init__(self, task_queue: Queue, result_queue: Queue): super().__init__(daemon=True, name="GraphCreator") self.task_queue = task_queue self.result_queue = result_queue def run(self): while True: settings, percentages = self.task_queue.get() if None in [settings, percentages]: break result = self.generate_graph(settings, percentages) self.result_queue.put(result) print("GraphCreator stopped") def generate_graph(self, settings: dict, percentages: list[float]): line_color = self.conv_color_to_plt(settings.get("line-color", [255, 255, 255, 255])) fill_color = self.conv_color_to_plt(settings.get("fill-color", [255, 255, 255, 150])) line_width = settings.get("line-width", 5) dynamic_scaling = settings.get("dynamic-scaling", False) # Create a new figure with a transparent background fig = plt.figure(figsize=(6, 6)) fig.patch.set_alpha(0) fig.patch.set_facecolor('none') # Set the FigureCanvas to the backend canvas = FigureCanvas(fig) # Plot the data with a transparent background ax = plt.Axes(fig, [0., 0., 1., 1.]) ax.set_axis_off() fig.add_axes(ax) ax.plot(percentages, color=line_color, linewidth=line_width) ax.fill_between(range(len(percentages)), percentages, color=fill_color[:3], alpha=fill_color[3]) # Hide the spines for spine in ax.spines.values(): spine.set_visible(False) # Turn off the axis and set margins to zero ax.margins(0) ax.axis('off') # Set the y-axis to range from 0 to 100 if not dynamic_scaling: ax.set_ylim(0, 100) # Draw the canvas and retrieve the buffer canvas.draw() buf = io.BytesIO() canvas.print_png(buf) # Convert buffer to a Pillow Image buf.seek(0) img = Image.open(buf) # The Pillow Image now has a transparent background # img.show() # If you want to display the image # img.save('plot.png') # If you want to save the image plt.close() # Close the plot to free resources # result_queue.put(img) return img def conv_color_to_plt(self, color: list[int]) -> list[float]: float_color: list[float] = [] for c in color: float_color.append(c / 255) return float_color