This commit is contained in:
2026-06-03 21:32:45 +02:00
parent f2328ff319
commit 1e869b49c7
126 changed files with 41986 additions and 1 deletions
+364
View File
@@ -0,0 +1,364 @@
import json
import os
from pathlib import Path
import cairo
import gi
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
import config.data as data
import modules.icons as icons
gi.require_version('Gtk', '3.0')
from gi.repository import Gdk, GLib, GObject, Gtk
def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
alloc = widget.get_allocation()
surface = cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height)
cr = cairo.Context(surface)
cr.set_source_rgba(0, 0, 0, 0)
cr.rectangle(0, 0, alloc.width, alloc.height)
cr.fill()
widget.draw(cr)
return surface
class InlineEditor(Gtk.Box):
__gsignals__ = {
'confirmed': (GObject.SignalFlags.RUN_LAST, None, (str,)),
'canceled': (GObject.SignalFlags.RUN_LAST, None, ())
}
def __init__(self, initial_text=""):
super().__init__(name="inline-editor", spacing=4)
self.text_view = Gtk.TextView()
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
buffer = self.text_view.get_buffer()
buffer.set_text(initial_text)
self.text_view.connect("key-press-event", self.on_key_press)
confirm_btn = Gtk.Button(name="kanban-btn", child=Label(name="kanban-btn-label", markup=icons.accept))
confirm_btn.connect("clicked", self.on_confirm)
confirm_btn.get_style_context().add_class("flat")
cancel_btn = Gtk.Button(name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.cancel))
cancel_btn.connect("clicked", self.on_cancel)
cancel_btn.get_style_context().add_class("flat")
sw = ScrolledWindow(name="scrolled-window", propagate_height=False, propagate_width=False)
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
sw.set_min_content_height(50)
sw.add(self.text_view)
self.button_box = Box(children=[confirm_btn, cancel_btn], spacing=4)
self.center_box = CenterBox(center_children=[self.button_box], orientation="v")
self.pack_start(sw, True, True, 0)
self.pack_start(self.center_box, False, False, 0)
self.show_all()
def on_confirm(self, widget):
buffer = self.text_view.get_buffer()
start, end = buffer.get_bounds()
text = buffer.get_text(start, end, True).strip()
if text:
self.emit('confirmed', text)
else:
self.emit('canceled')
def on_cancel(self, widget):
self.emit('canceled')
def on_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.emit('canceled')
return True
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
state = event.get_state()
if state & Gdk.ModifierType.SHIFT_MASK:
buffer = self.text_view.get_buffer()
cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
buffer.insert(cursor_iter, "\n")
return True
else:
self.on_confirm(widget)
return True
return False
class KanbanNote(Gtk.EventBox):
__gsignals__ = {
'changed': (GObject.SignalFlags.RUN_LAST, None, ()),
}
def __init__(self, text):
super().__init__()
self.text = text
self.setup_ui()
self.setup_dnd()
self.connect("button-press-event", self.on_button_press)
def setup_ui(self):
self.box = Gtk.Box(name="kanban-note", spacing=4)
self.label = Gtk.Label(label=self.text)
self.label.set_line_wrap(True)
self.label.set_line_wrap_mode(Gtk.WrapMode.WORD)
self.delete_btn = Gtk.Button(name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.trash))
self.delete_btn.connect("clicked", self.on_delete_clicked)
self.center_btn = CenterBox(orientation="v", start_children=[self.delete_btn])
self.box.pack_start(self.label, True, True, 0)
self.box.pack_start(self.center_btn, False, False, 0)
self.add(self.box)
self.show_all()
def setup_dnd(self):
self.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new('UTF8_STRING', Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE
)
self.connect("drag-data-get", self.on_drag_data_get)
self.connect("drag-data-delete", self.on_drag_data_delete)
self.connect("drag-begin", self.on_drag_begin)
def on_button_press(self, widget, event):
if event.type != Gdk.EventType._2BUTTON_PRESS:
return True
self.start_edit()
return False
def on_drag_begin(self, widget, context):
surface = createSurfaceFromWidget(self)
Gtk.drag_set_icon_surface(context, surface)
def on_drag_data_get(self, widget, drag_context, data, info, time):
data.set_text(self.label.get_text(), -1)
def on_drag_data_delete(self, widget, drag_context):
self.get_parent().destroy()
def on_delete_clicked(self, button):
self.get_parent().destroy()
def start_edit(self):
row = self.get_parent()
editor = InlineEditor(self.label.get_text())
def on_confirmed(editor, text):
self.label.set_text(text)
row.remove(editor)
row.add(self)
row.show_all()
self.emit('changed')
def on_canceled(editor):
row.remove(editor)
row.add(self)
row.show_all()
editor.connect('confirmed', on_confirmed)
editor.connect('canceled', on_canceled)
row.remove(self)
row.add(editor)
row.show_all()
GLib.timeout_add(50, lambda: (editor.text_view.grab_focus(), False))
class KanbanColumn(Gtk.Frame):
__gsignals__ = {
'changed': (GObject.SignalFlags.RUN_LAST, None, ()),
}
def __init__(self, title):
super().__init__(name="kanban-column")
self.title = title
self.setup_ui()
self.setup_dnd()
self.set_hexpand(True)
self.set_vexpand(True)
def setup_ui(self):
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
self.listbox = Gtk.ListBox()
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
self.add_btn = Gtk.Button(name="kanban-btn-add", child=Label(name="kanban-btn-label", markup=icons.add))
header = CenterBox(name="kanban-header", center_children=[Label(name="column-header", label=self.title)], end_children=[self.add_btn])
self.box.pack_start(header, False, False, 0)
self.add_btn.connect("clicked", self.on_add_clicked)
self.scroller = ScrolledWindow(name="scrolled-window", propagate_height=False, propagate_width=False)
self.scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self.scroller.add(self.listbox)
self.scroller.set_vexpand(True)
self.box.pack_start(self.scroller, True, True, 0)
self.box.pack_start(self.add_btn, False, False, 0)
self.add(self.box)
self.show_all()
def setup_dnd(self):
self.listbox.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new('UTF8_STRING', Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE
)
self.listbox.connect("drag-data-received", self.on_drag_data_received)
self.listbox.connect("drag-motion", self.on_drag_motion)
self.listbox.connect("drag-leave", self.on_drag_leave)
def on_add_clicked(self, button):
editor = InlineEditor()
row = Gtk.ListBoxRow(name="kanban-row")
row.add(editor)
self.listbox.add(row)
self.listbox.show_all()
editor.text_view.grab_focus()
def on_confirmed(editor, text):
note = KanbanNote(text)
note.connect('changed', lambda x: self.emit('changed'))
row.remove(editor)
row.add(note)
self.listbox.show_all()
self.emit('changed')
def on_canceled(editor):
row.destroy()
def scroll_to_bottom():
adj = self.scroller.get_vadjustment()
adj.set_value(adj.get_upper())
editor.connect('confirmed', on_confirmed)
editor.connect('canceled', on_canceled)
GLib.idle_add(scroll_to_bottom) # ensure this is called after row is loaded
def add_note(self, text, suppress_signal=False):
note = KanbanNote(text)
note.connect('changed', lambda x: self.emit('changed'))
row = Gtk.ListBoxRow(name="kanban-row")
row.add(note)
row.connect('destroy', lambda x: self.emit('changed'))
self.listbox.add(row)
self.listbox.show_all()
if not suppress_signal:
self.emit('changed')
def get_notes(self):
return [
row.get_children()[0].label.get_text()
for row in self.listbox.get_children()
if isinstance(row.get_children()[0], KanbanNote)
]
def clear_notes(self, suppress_signal=False):
for row in self.listbox.get_children():
row.destroy()
if not suppress_signal:
self.emit('changed')
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
text = data.get_text()
if text:
row = self.listbox.get_row_at_y(y)
new_note = KanbanNote(text)
new_note.connect('changed', lambda x: self.emit('changed'))
new_row = Gtk.ListBoxRow(name="kanban-row")
new_row.add(new_note)
new_row.connect('destroy', lambda x: self.emit('changed'))
if row:
self.listbox.insert(new_row, row.get_index())
else:
self.listbox.add(new_row)
self.listbox.show_all()
drag_context.finish(True, False, time)
self.emit('changed')
def on_drag_motion(self, widget, drag_context, x, y, time):
Gdk.drag_status(drag_context, Gdk.DragAction.MOVE, time)
return True
def on_drag_leave(self, widget, drag_context, time):
widget.get_parent().get_parent().drag_unhighlight()
class Kanban(Gtk.Box):
STATE_FILE = Path(os.path.expanduser("~/.kanban.json"))
def __init__(self):
super().__init__(name="kanban")
self.grid = Gtk.Grid(column_spacing=4, column_homogeneous=True, row_spacing=4, row_homogeneous=True)
self.grid.set_vexpand(True)
self.add(self.grid)
self.columns = [
KanbanColumn("To Do"),
KanbanColumn("In Progress"),
KanbanColumn("Done")
]
vertical_mode = True if data.PANEL_THEME == "Panel" and (data.BAR_POSITION in ["Left", "Right"] or data.PANEL_POSITION in ["Start", "End"]) else False
for i, column in enumerate(self.columns):
if vertical_mode == False:
self.grid.attach(column, i, 0, 1, 1)
else:
self.grid.attach(column, 0, i, 1, 1)
column.connect('changed', lambda x: self.save_state())
self.load_state()
self.show_all()
def save_state(self):
state = {
"columns": [
{"title": col.title, "notes": col.get_notes()}
for col in self.columns
]
}
try:
with open(self.STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
except Exception as e:
print(f"Error saving state: {e}")
def load_state(self):
try:
with open(self.STATE_FILE, "r") as f:
state = json.load(f)
for col_data in state["columns"]:
for column in self.columns:
if column.title == col_data["title"]:
column.clear_notes(suppress_signal=True)
for note_text in col_data["notes"]:
column.add_note(note_text, suppress_signal=True)
break
except FileNotFoundError:
pass
except Exception as e:
print(f"Error loading state: {e}")