hourtrack/hourtrack/compo/TriageView.py
2025-01-27 00:37:37 +09:00

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)