215 lines
5.9 KiB
Python
215 lines
5.9 KiB
Python
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from imgui_bundle import imgui, imgui_ctx
|
|
|
|
|
|
PRIORITY_LABELS = ["A", "B", "C", "D"]
|
|
DND_PAYLOAD_TYPE = "TICKET"
|
|
|
|
|
|
# ---- data types ----
|
|
TicketId = int
|
|
|
|
@dataclass
|
|
class Ticket:
|
|
id : TicketId = 0
|
|
summary : str = "this is tooooo too long summary"
|
|
selected: bool = False
|
|
|
|
|
|
# ---- volatile ----
|
|
@dataclass
|
|
class VolatileData:
|
|
selected: TicketId = 0
|
|
|
|
editing_new_summaries: list[str] = field(default_factory=lambda: ["", "", "", ""])
|
|
|
|
|
|
# ---- I/O ----
|
|
class IO(ABC):
|
|
@abstractmethod
|
|
def get_tickets(self) -> list[list[Ticket]]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def move_ticket(self, t: TicketId, priority: int, index: int) -> bool:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def select_ticket(self, t: TicketId) -> None:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def add_ticket(self, priority: int, summary: str) -> TicketId:
|
|
pass
|
|
|
|
|
|
class SimpleIO(IO):
|
|
_items = [
|
|
[Ticket(0), Ticket(1), Ticket(2)],
|
|
[Ticket(3)],
|
|
[Ticket(4), Ticket(5)],
|
|
[],
|
|
]
|
|
_next_id = 100
|
|
|
|
def get_tickets(self) -> list[list[Ticket]]:
|
|
return self._items
|
|
|
|
def move_ticket(self, t: TicketId, priority: int, index: int) -> bool:
|
|
for pi,pl in enumerate(self._items):
|
|
i = next((i for i,v in enumerate(pl) if v.id == t), None)
|
|
if i is not None:
|
|
self._items[priority].insert(index, pl[i])
|
|
if priority == pi and i > index:
|
|
i += 1
|
|
del pl[i]
|
|
return True
|
|
return False
|
|
|
|
def select_ticket(self, tid: TicketId) -> None:
|
|
for pl in self._items:
|
|
for t in pl:
|
|
t.selected = (tid == t.id)
|
|
|
|
def add_ticket(self, priority: int, summary: str) -> TicketId:
|
|
id = self._next_id
|
|
self._next_id += 1
|
|
self._items[priority].append(Ticket(id, summary))
|
|
return id
|
|
|
|
|
|
# ---- component definition ----
|
|
@dataclass
|
|
class Context:
|
|
io : IO
|
|
vola: VolatileData = field(default_factory=lambda: VolatileData())
|
|
|
|
def compo(ctx: Context) -> None:
|
|
io = ctx.io
|
|
vola = ctx.vola
|
|
tickets = io.get_tickets()
|
|
style = imgui.get_style()
|
|
|
|
selected = None
|
|
dragging = imgui.get_drag_drop_payload_py_id()
|
|
if dragging is not None:
|
|
dragging = dragging.data_id if dragging.type == DND_PAYLOAD_TYPE else None
|
|
|
|
ln = len(PRIORITY_LABELS)
|
|
lw = (imgui.get_content_region_avail().x - style.frame_padding.x * (ln+1)) / ln
|
|
for pi,pl in enumerate(PRIORITY_LABELS):
|
|
with imgui_ctx.begin_child(pl, (lw, 0), imgui.ChildFlags_.borders):
|
|
imgui.text(pl)
|
|
|
|
basepos = imgui.get_cursor_screen_pos()
|
|
lh = imgui.get_content_region_avail().y - imgui.get_frame_height_with_spacing()
|
|
with imgui_ctx.begin_child("list", (0, lh), imgui.ChildFlags_.borders):
|
|
seps = []
|
|
for ticket in tickets[pi]:
|
|
seps.append(imgui.get_cursor_pos_y())
|
|
|
|
with imgui_ctx.push_id(ticket.id):
|
|
_compo_ticket(
|
|
ticket,
|
|
selected=ticket.selected,
|
|
gray=(dragging == ticket.id),
|
|
)
|
|
if ticket.selected:
|
|
selected = ticket.id
|
|
if vola.selected != ticket.id:
|
|
imgui.set_scroll_here_x()
|
|
imgui.set_scroll_here_y()
|
|
|
|
if imgui.is_item_clicked():
|
|
io.select_ticket(ticket.id)
|
|
selected = ticket.id
|
|
|
|
flags = imgui.DragDropFlags_.source_allow_null_id
|
|
if imgui.begin_drag_drop_source(flags):
|
|
imgui.set_drag_drop_payload_py_id(DND_PAYLOAD_TYPE, ticket.id)
|
|
_compo_ticket_detail(ticket)
|
|
imgui.end_drag_drop_source()
|
|
seps.append(imgui.get_cursor_pos_y())
|
|
|
|
if imgui.begin_drag_drop_target():
|
|
my = imgui.get_mouse_pos().y - basepos.y
|
|
if dragging is not None:
|
|
# find the nearest pos to insert
|
|
idx, pos = min(enumerate(seps), key=lambda d: abs(d[1]-my))
|
|
|
|
# draws line showing pos to insert
|
|
y = basepos.y + pos - style.item_spacing.y/2
|
|
imgui.get_window_draw_list().add_line(
|
|
(imgui.get_item_rect_min().x, y),
|
|
(imgui.get_item_rect_max().x, y),
|
|
imgui.get_color_u32((1,1,1,1)),
|
|
)
|
|
|
|
# completes the moving
|
|
if imgui.accept_drag_drop_payload_py_id(DND_PAYLOAD_TYPE) is not None:
|
|
io.move_ticket(dragging, pi, idx)
|
|
imgui.end_drag_drop_target()
|
|
|
|
# new ticket creation
|
|
imgui.set_next_item_width(-1)
|
|
entered, vola.editing_new_summaries[pi] = imgui.input_text_with_hint(
|
|
"##new_summary",
|
|
"new ticket summary...",
|
|
vola.editing_new_summaries[pi],
|
|
imgui.InputTextFlags_.enter_returns_true,
|
|
)
|
|
if entered:
|
|
io.add_ticket(pi, vola.editing_new_summaries[pi])
|
|
vola.editing_new_summaries[pi] = ""
|
|
imgui.same_line()
|
|
|
|
vola.selected = selected
|
|
|
|
def _compo_ticket(t: Ticket, selected: bool = False, gray: bool = False) -> None:
|
|
style = imgui.get_style()
|
|
ltop = imgui.get_cursor_screen_pos()
|
|
avail = imgui.get_content_region_avail()
|
|
dlist = imgui.get_window_draw_list()
|
|
|
|
with imgui_ctx.begin_group():
|
|
imgui.dummy((avail.x, 0))
|
|
imgui.dummy((0, 0))
|
|
imgui.same_line()
|
|
imgui.text_disabled(f"#{t.id}")
|
|
imgui.same_line()
|
|
imgui.text_wrapped(t.summary)
|
|
imgui.same_line()
|
|
imgui.dummy((0, 0))
|
|
imgui.dummy((avail.x, 0))
|
|
|
|
# tooltip
|
|
if imgui.is_item_hovered():
|
|
imgui.set_mouse_cursor(imgui.MouseCursor_.hand)
|
|
with imgui_ctx.begin_tooltip():
|
|
_compo_ticket_detail(t)
|
|
|
|
# border
|
|
dlist.add_rect(
|
|
imgui.get_item_rect_min(),
|
|
imgui.get_item_rect_max(),
|
|
imgui.get_color_u32((1,1,1,1)),
|
|
style.frame_rounding,
|
|
)
|
|
|
|
# background
|
|
bg = None
|
|
if gray : bg = (.0,.0,.0,.4)
|
|
if selected: bg = (.4,.4,.8,.4)
|
|
if bg is not None:
|
|
dlist.add_rect_filled(
|
|
imgui.get_item_rect_min(),
|
|
imgui.get_item_rect_max(),
|
|
imgui.get_color_u32(bg),
|
|
style.frame_rounding,
|
|
)
|
|
|
|
def _compo_ticket_detail(t: Ticket):
|
|
imgui.text_disabled(f"#{t.id}")
|
|
imgui.text(t.summary)
|