Initial commit: Bcut-style Taiko Editor
This commit is contained in:
617
ui.py
Normal file
617
ui.py
Normal file
@@ -0,0 +1,617 @@
|
||||
import pygame
|
||||
import pygame.gfxdraw
|
||||
from editor_core import EditorState
|
||||
import ui_utils
|
||||
from ui_utils import FontManager, COLOR_BG_ROOT, COLOR_BG_PANEL, COLOR_BORDER, COLOR_ACCENT, COLOR_TEXT_MAIN, COLOR_TEXT_DIM, COLOR_TIMELINE_BG, COLOR_TRACK_BG
|
||||
|
||||
# Note Colors
|
||||
COLOR_DON = (255, 71, 87)
|
||||
COLOR_KA = (65, 166, 245)
|
||||
COLOR_ROLL = (255, 200, 50)
|
||||
|
||||
class UI:
|
||||
def __init__(self, editor: EditorState, screen_width, screen_height):
|
||||
self.editor = editor
|
||||
self.width = screen_width
|
||||
self.height = screen_height
|
||||
self.font_mgr = FontManager()
|
||||
|
||||
# State
|
||||
self.dragging_note = None
|
||||
self.is_dragging_timeline = False
|
||||
self.last_mouse_x = 0
|
||||
self.hovered_ui_item = None
|
||||
|
||||
# Input/Edit State
|
||||
self.input_active = None # 'BPM' or 'TITLE' or None
|
||||
self.input_text = ""
|
||||
self.input_rect = None
|
||||
|
||||
# Key Recording State
|
||||
self.key_buffer = {} # {key: press_time}
|
||||
|
||||
# Layout Rects (calculated in update_layout)
|
||||
self.update_layout(screen_width, screen_height)
|
||||
|
||||
def update_layout(self, w, h):
|
||||
self.width = w
|
||||
self.height = h
|
||||
|
||||
# 1. Header (Top)
|
||||
self.h_header = 40
|
||||
self.rect_header = pygame.Rect(0, 0, w, self.h_header)
|
||||
|
||||
# 2. Main Body (Middle)
|
||||
self.h_timeline = int(h * 0.38)
|
||||
self.h_main = h - self.h_header - self.h_timeline
|
||||
self.y_main = self.h_header
|
||||
|
||||
# 3. Columns in Main Body
|
||||
self.w_lib = 240
|
||||
self.w_prop = 260
|
||||
self.w_preview = w - self.w_lib - self.w_prop
|
||||
|
||||
self.rect_lib = pygame.Rect(0, self.y_main, self.w_lib, self.h_main)
|
||||
self.rect_preview = pygame.Rect(self.w_lib, self.y_main, self.w_preview, self.h_main)
|
||||
self.rect_prop = pygame.Rect(self.w_lib + self.w_preview, self.y_main, self.w_prop, self.h_main)
|
||||
|
||||
# 4. Timeline (Bottom)
|
||||
self.rect_timeline = pygame.Rect(0, self.y_main + self.h_main, w, self.h_timeline)
|
||||
|
||||
def draw(self, screen):
|
||||
screen.fill(COLOR_BG_ROOT)
|
||||
|
||||
self.draw_header(screen)
|
||||
self.draw_library(screen)
|
||||
self.draw_preview(screen)
|
||||
self.draw_properties(screen)
|
||||
self.draw_timeline(screen)
|
||||
|
||||
# Drag Overlay
|
||||
if self.dragging_note:
|
||||
mx, my = pygame.mouse.get_pos()
|
||||
ntype = self.dragging_note['type']
|
||||
color = COLOR_DON if ntype in [1, 3] else COLOR_KA
|
||||
r = 24 if ntype in [3, 4] else 16
|
||||
|
||||
# Draw ghost
|
||||
pygame.gfxdraw.filled_circle(screen, mx, my, r+4, (255, 255, 255, 100))
|
||||
pygame.gfxdraw.aacircle(screen, mx, my, r, color)
|
||||
pygame.gfxdraw.filled_circle(screen, mx, my, r, color)
|
||||
|
||||
def draw_panel_bg(self, screen, rect):
|
||||
# Fill
|
||||
pygame.draw.rect(screen, COLOR_BG_PANEL, rect)
|
||||
# Border
|
||||
pygame.draw.rect(screen, COLOR_BORDER, rect, 1)
|
||||
|
||||
def draw_header(self, screen):
|
||||
r = self.rect_header
|
||||
pygame.draw.rect(screen, COLOR_BG_PANEL, r)
|
||||
pygame.draw.line(screen, COLOR_BORDER, r.bottomleft, r.bottomright)
|
||||
|
||||
# Title
|
||||
font_title = self.font_mgr.get_font(18, bold=True)
|
||||
title_surf = font_title.render("Taiko Editor", True, COLOR_ACCENT)
|
||||
screen.blit(title_surf, (15, (self.h_header - title_surf.get_height())//2))
|
||||
|
||||
# Difficulty Tabs (Pills)
|
||||
diffs = ['Easy', 'Normal', 'Hard', 'Oni', 'Edit']
|
||||
cx = 160
|
||||
cy = self.h_header // 2
|
||||
self.tab_rects = {} # Store rects for hit testing
|
||||
|
||||
for d in diffs:
|
||||
is_active = (d == self.editor.current_course_name)
|
||||
txt_font = self.font_mgr.get_font(12)
|
||||
tw, th = txt_font.size(d)
|
||||
btn_w = tw + 20
|
||||
btn_h = 24
|
||||
btn_rect = pygame.Rect(cx, cy - btn_h//2, btn_w, btn_h)
|
||||
self.tab_rects[d] = btn_rect
|
||||
|
||||
ui_utils.draw_pill_btn(screen, d, btn_rect, is_active, self.font_mgr)
|
||||
|
||||
cx += btn_w + 10
|
||||
|
||||
# Export Button (Right)
|
||||
btn_exp_w = 80
|
||||
btn_exp_h = 26
|
||||
self.rect_exp = pygame.Rect(self.width - btn_exp_w - 15, cy - btn_exp_h//2, btn_exp_w, btn_exp_h)
|
||||
# Draw rounded rect
|
||||
pygame.draw.rect(screen, (0, 180, 210), self.rect_exp, border_radius=4)
|
||||
exp_txt = self.font_mgr.get_font(12, bold=True).render("导出", True, (0, 0, 0))
|
||||
screen.blit(exp_txt, (self.rect_exp.x + (btn_exp_w - exp_txt.get_width())//2, self.rect_exp.y + 4))
|
||||
|
||||
def draw_library(self, screen):
|
||||
r = self.rect_lib
|
||||
self.draw_panel_bg(screen, r)
|
||||
|
||||
# Sidebar (Icons)
|
||||
sidebar_w = 40
|
||||
pygame.draw.rect(screen, (25, 25, 25), (r.x, r.y, sidebar_w, r.h))
|
||||
pygame.draw.line(screen, COLOR_BORDER, (r.x + sidebar_w, r.y), (r.x + sidebar_w, r.y + r.h))
|
||||
|
||||
# Main Area Title
|
||||
font = self.font_mgr.get_font(14)
|
||||
lbl = font.render("素材库", True, COLOR_TEXT_MAIN)
|
||||
screen.blit(lbl, (r.x + sidebar_w + 10, r.y + 10))
|
||||
|
||||
# Assets Grid
|
||||
start_x = r.x + sidebar_w + 15
|
||||
start_y = r.y + 40
|
||||
|
||||
assets = [
|
||||
{'name': 'Don', 'type': 1, 'color': COLOR_DON},
|
||||
{'name': 'Ka', 'type': 2, 'color': COLOR_KA},
|
||||
{'name': 'Big Don', 'type': 3, 'color': COLOR_DON},
|
||||
{'name': 'Big Ka', 'type': 4, 'color': COLOR_KA},
|
||||
{'name': 'Roll', 'type': 5, 'color': COLOR_ROLL},
|
||||
]
|
||||
|
||||
x, y = start_x, start_y
|
||||
for item in assets:
|
||||
# Card Bg
|
||||
card_w, card_h = 80, 80
|
||||
card_rect = pygame.Rect(x, y, card_w, card_h)
|
||||
|
||||
mx, my = pygame.mouse.get_pos()
|
||||
hover = card_rect.collidepoint(mx, my)
|
||||
|
||||
bg_col = (50, 50, 50) if hover else (40, 40, 40)
|
||||
pygame.draw.rect(screen, bg_col, card_rect, border_radius=6)
|
||||
|
||||
# Icon
|
||||
cx, cy = card_rect.centerx, card_rect.centery - 10
|
||||
rad = 18 if item['type'] in [3, 4] else 12
|
||||
pygame.gfxdraw.aacircle(screen, cx, cy, rad, item['color'])
|
||||
pygame.gfxdraw.filled_circle(screen, cx, cy, rad, item['color'])
|
||||
|
||||
# Text
|
||||
name_surf = self.font_mgr.get_font(12).render(item['name'], True, COLOR_TEXT_DIM)
|
||||
screen.blit(name_surf, (cx - name_surf.get_width()//2, cy + 20))
|
||||
|
||||
x += card_w + 10
|
||||
if x + card_w > r.right:
|
||||
x = start_x
|
||||
y += card_h + 10
|
||||
|
||||
# Handle Drag Start
|
||||
if hover and pygame.mouse.get_pressed()[0]:
|
||||
self.dragging_note = {'type': item['type']}
|
||||
|
||||
def draw_preview(self, screen):
|
||||
r = self.rect_preview
|
||||
# Draw pure black bg for player
|
||||
pygame.draw.rect(screen, (0, 0, 0), r)
|
||||
# Border
|
||||
pygame.draw.rect(screen, COLOR_BORDER, r, 1)
|
||||
|
||||
# Placeholder Waveform/Visual
|
||||
cx, cy = r.center
|
||||
pygame.draw.line(screen, (50, 50, 50), (r.x, cy), (r.right, cy))
|
||||
|
||||
# Song Title
|
||||
title = self.editor.tja.headers.get('TITLE', 'No Title')
|
||||
font_big = self.font_mgr.get_font(24, bold=True)
|
||||
surf = font_big.render(title, True, (100, 100, 100))
|
||||
screen.blit(surf, (cx - surf.get_width()//2, cy - 50))
|
||||
|
||||
# Controls Bar (Bottom of preview)
|
||||
ctrl_h = 40
|
||||
ctrl_y = r.bottom - ctrl_h
|
||||
pygame.draw.rect(screen, (20, 20, 20), (r.x, ctrl_y, r.w, ctrl_h))
|
||||
|
||||
# Play Button
|
||||
p_center = (r.x + 30, ctrl_y + 20)
|
||||
color_play = COLOR_TEXT_MAIN
|
||||
if self.editor.is_playing:
|
||||
ui_utils.draw_icon_pause(screen, p_center, 16, color_play)
|
||||
else:
|
||||
ui_utils.draw_icon_play(screen, p_center, 16, color_play)
|
||||
|
||||
# Timecode
|
||||
ms = self.editor.current_time
|
||||
if ms < 0: ms = 0
|
||||
secs = int(ms / 1000)
|
||||
mins = secs // 60
|
||||
secs = secs % 60
|
||||
msecs = int(ms % 1000) // 10
|
||||
time_str = f"{mins:02}:{secs:02}:{msecs:02}"
|
||||
|
||||
font_time = self.font_mgr.get_font(14, bold=True) # Monospaced ideally
|
||||
tsurf = font_time.render(time_str, True, COLOR_ACCENT)
|
||||
screen.blit(tsurf, (r.x + 60, ctrl_y + 10))
|
||||
|
||||
def draw_properties(self, screen):
|
||||
r = self.rect_prop
|
||||
self.draw_panel_bg(screen, r)
|
||||
|
||||
font_head = self.font_mgr.get_font(14, bold=True)
|
||||
font_lbl = self.font_mgr.get_font(12)
|
||||
|
||||
pad = 15
|
||||
y = r.y + pad
|
||||
|
||||
# Title
|
||||
screen.blit(font_head.render("属性", True, COLOR_TEXT_MAIN), (r.x + pad, y))
|
||||
y += 30
|
||||
|
||||
# Content
|
||||
if self.editor.selected_notes:
|
||||
# Note Properties
|
||||
note = self.editor.selected_notes[0]
|
||||
props = [
|
||||
("类型", str(note.type)),
|
||||
("时间", f"{note.time:.1f}ms"),
|
||||
("位置", f"{note.measure + 1} - {note.beat + 1:.1f}"),
|
||||
]
|
||||
|
||||
for k, v in props:
|
||||
screen.blit(font_lbl.render(k, True, COLOR_TEXT_DIM), (r.x + pad, y))
|
||||
screen.blit(font_lbl.render(v, True, COLOR_TEXT_MAIN), (r.x + pad + 60, y))
|
||||
y += 25
|
||||
|
||||
# Delete Hint
|
||||
y += 20
|
||||
hint = font_lbl.render("右键删除 / Del键", True, (150, 50, 50))
|
||||
screen.blit(hint, (r.x + pad, y))
|
||||
|
||||
else:
|
||||
# Global Properties
|
||||
# Use stored rects for click detection
|
||||
self.prop_rects = {}
|
||||
|
||||
props = [
|
||||
("BPM", str(self.editor.tja.headers.get('BPM')), 'BPM'),
|
||||
("OFFSET", str(self.editor.tja.headers.get('OFFSET')), 'OFFSET'),
|
||||
("标题", str(self.editor.tja.headers.get('TITLE')), 'TITLE'),
|
||||
("难度", self.editor.current_course_name, None),
|
||||
]
|
||||
|
||||
for k, v, key_id in props:
|
||||
screen.blit(font_lbl.render(k, True, COLOR_TEXT_DIM), (r.x + pad, y))
|
||||
|
||||
# Value box
|
||||
val_rect = pygame.Rect(r.x + pad + 60, y - 2, 140, 24)
|
||||
if key_id: self.prop_rects[key_id] = val_rect
|
||||
|
||||
is_editing = (self.input_active == key_id)
|
||||
bg_col = (50, 50, 70) if is_editing else (20, 20, 20)
|
||||
|
||||
pygame.draw.rect(screen, bg_col, val_rect, border_radius=4)
|
||||
|
||||
disp_text = self.input_text if is_editing else v
|
||||
screen.blit(font_lbl.render(disp_text, True, COLOR_TEXT_MAIN), (val_rect.x + 5, val_rect.y + 4))
|
||||
|
||||
if is_editing:
|
||||
# Cursor
|
||||
tw = font_lbl.size(disp_text)[0]
|
||||
pygame.draw.line(screen, COLOR_ACCENT, (val_rect.x + 5 + tw, val_rect.y+4), (val_rect.x + 5 + tw, val_rect.y+20))
|
||||
|
||||
y += 35
|
||||
|
||||
def draw_timeline(self, screen):
|
||||
r = self.rect_timeline
|
||||
pygame.draw.rect(screen, COLOR_TIMELINE_BG, r)
|
||||
pygame.draw.line(screen, COLOR_BORDER, r.topleft, r.topright)
|
||||
|
||||
# Header (Track Names)
|
||||
w_header = 100
|
||||
rect_track_header = pygame.Rect(r.x, r.y, w_header, r.h)
|
||||
pygame.draw.rect(screen, (30, 30, 30), rect_track_header)
|
||||
pygame.draw.line(screen, COLOR_BORDER, rect_track_header.topright, rect_track_header.bottomright)
|
||||
|
||||
# Tracks
|
||||
track_h = 80
|
||||
y_track = r.y + 30 # 30px ruler
|
||||
|
||||
# Ruler
|
||||
self.draw_ruler(screen, r.x + w_header, r.y, r.w - w_header, 30)
|
||||
|
||||
# Note Track
|
||||
rect_track_bg = pygame.Rect(r.x + w_header, y_track, r.w - w_header, track_h)
|
||||
pygame.draw.rect(screen, COLOR_TRACK_BG, rect_track_bg)
|
||||
pygame.draw.line(screen, COLOR_BORDER, (r.x, y_track+track_h), (r.right, y_track+track_h))
|
||||
|
||||
# Label
|
||||
lbl = self.font_mgr.get_font(12).render("音符轨", True, COLOR_TEXT_DIM)
|
||||
screen.blit(lbl, (r.x + 10, y_track + 30))
|
||||
|
||||
# Draw Grid & Notes
|
||||
self.draw_track_content(screen, rect_track_bg)
|
||||
|
||||
def draw_ruler(self, screen, x, y, w, h):
|
||||
pygame.draw.rect(screen, (40, 40, 40), (x, y, w, h))
|
||||
pygame.draw.line(screen, COLOR_BORDER, (x, y+h), (x+w, y+h))
|
||||
|
||||
bpm = self.editor.tja.headers.get('BPM', 120)
|
||||
beat_ms = 60000 / bpm
|
||||
|
||||
# Visible range
|
||||
t_start = self.x_to_time(x, x, w)
|
||||
t_end = self.x_to_time(x+w, x, w)
|
||||
|
||||
beat_start = int(t_start / beat_ms)
|
||||
beat_end = int(t_end / beat_ms) + 1
|
||||
|
||||
for i in range(beat_start, beat_end):
|
||||
t = i * beat_ms
|
||||
draw_x = self.time_to_x(t, x, w)
|
||||
|
||||
if x <= draw_x <= x + w:
|
||||
is_measure = (i % 4 == 0)
|
||||
h_line = 15 if is_measure else 8
|
||||
col = (200, 200, 200) if is_measure else (100, 100, 100)
|
||||
pygame.draw.line(screen, col, (draw_x, y + h - h_line), (draw_x, y + h))
|
||||
|
||||
if is_measure:
|
||||
num = i // 4
|
||||
txt = self.font_mgr.get_font(10).render(str(num), True, (150, 150, 150))
|
||||
screen.blit(txt, (draw_x + 2, y + 2))
|
||||
|
||||
def draw_track_content(self, screen, rect):
|
||||
# Clip area
|
||||
prev_clip = screen.get_clip()
|
||||
screen.set_clip(rect)
|
||||
|
||||
bpm = self.editor.tja.headers.get('BPM', 120)
|
||||
beat_ms = 60000 / bpm
|
||||
|
||||
center_y = rect.centery
|
||||
|
||||
# Grid Lines
|
||||
t_start = self.x_to_time(rect.x, rect.x, rect.w)
|
||||
t_end = self.x_to_time(rect.right, rect.x, rect.w)
|
||||
|
||||
beat_start = int(t_start / beat_ms)
|
||||
beat_end = int(t_end / beat_ms) + 1
|
||||
|
||||
for i in range(beat_start, beat_end):
|
||||
t = i * beat_ms
|
||||
draw_x = self.time_to_x(t, rect.x, rect.w)
|
||||
is_measure = (i % 4 == 0)
|
||||
col = (60, 60, 60) if is_measure else (45, 45, 45)
|
||||
pygame.draw.line(screen, col, (draw_x, rect.top), (draw_x, rect.bottom))
|
||||
|
||||
# Notes
|
||||
course = self.editor.get_current_course()
|
||||
if course:
|
||||
visible_notes = [n for n in course.notes if t_start - 500 < n.time < t_end + 500]
|
||||
for note in visible_notes:
|
||||
draw_x = self.time_to_x(note.time, rect.x, rect.w)
|
||||
|
||||
color = COLOR_DON if note.type in [1, 3] else COLOR_KA
|
||||
if note.type == 5: color = COLOR_ROLL
|
||||
|
||||
radius = 22 if note.type in [3, 4] else 15
|
||||
|
||||
# Selection Glow
|
||||
if note in self.editor.selected_notes:
|
||||
pygame.gfxdraw.filled_circle(screen, int(draw_x), int(center_y), radius+4, (255, 255, 255, 100))
|
||||
pygame.draw.circle(screen, (255, 255, 255), (int(draw_x), int(center_y)), radius+2, 2)
|
||||
|
||||
# Note Body
|
||||
pygame.gfxdraw.aacircle(screen, int(draw_x), int(center_y), radius, (255, 255, 255)) # White Outline
|
||||
pygame.gfxdraw.filled_circle(screen, int(draw_x), int(center_y), radius, color)
|
||||
# Shine
|
||||
pygame.gfxdraw.filled_circle(screen, int(draw_x - radius*0.3), int(center_y - radius*0.3), int(radius*0.25), (255, 255, 255, 120))
|
||||
|
||||
# Playhead
|
||||
ph_x = self.time_to_x(self.editor.current_time, rect.x, rect.w)
|
||||
if rect.x <= ph_x <= rect.right:
|
||||
pygame.draw.line(screen, COLOR_ACCENT, (ph_x, rect.top - 10), (ph_x, rect.bottom), 2)
|
||||
# Cap
|
||||
pts = [(ph_x-6, rect.top-10), (ph_x+6, rect.top-10), (ph_x, rect.top)]
|
||||
pygame.draw.polygon(screen, COLOR_ACCENT, pts)
|
||||
|
||||
screen.set_clip(prev_clip)
|
||||
|
||||
# Coordinate Systems
|
||||
# We define: Center of timeline view corresponds to current_time
|
||||
# Playhead is FIXED at center of the track view area?
|
||||
# Bcut style: Playhead usually moves, or timeline scrolls.
|
||||
# Let's keep Fixed Playhead (Timeline Scrolls) for easier editing.
|
||||
|
||||
def time_to_x(self, t, track_x, track_w):
|
||||
center_x = track_x + track_w // 2
|
||||
return center_x + (t - self.editor.current_time) * self.editor.zoom_x
|
||||
|
||||
def x_to_time(self, x, track_x, track_w):
|
||||
center_x = track_x + track_w // 2
|
||||
return self.editor.current_time + (x - center_x) / self.editor.zoom_x
|
||||
|
||||
def handle_event(self, event):
|
||||
mx, my = pygame.mouse.get_pos()
|
||||
|
||||
# 0. Global Key Handling (Input vs Recording)
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if self.input_active:
|
||||
if event.key == pygame.K_RETURN:
|
||||
# Commit
|
||||
try:
|
||||
val = self.input_text
|
||||
if self.input_active in ['BPM', 'OFFSET']:
|
||||
val = float(val)
|
||||
self.editor.tja.headers[self.input_active] = val
|
||||
except:
|
||||
pass
|
||||
self.input_active = None
|
||||
elif event.key == pygame.K_BACKSPACE:
|
||||
self.input_text = self.input_text[:-1]
|
||||
elif event.key == pygame.K_ESCAPE:
|
||||
self.input_active = None
|
||||
else:
|
||||
self.input_text += event.unicode
|
||||
return # Consume event
|
||||
|
||||
# Recording Logic (F/J/D/K)
|
||||
# Logic: On key down, store time. If partner key pressed within 50ms, trigger Big.
|
||||
# Otherwise trigger small after timeout?
|
||||
# Or simplified: if F pressed, check if J is already down?
|
||||
# Better: When key pressed, add to buffer. On update loop, check buffer.
|
||||
# But event loop is easiest for immediate reaction.
|
||||
|
||||
# Mapping
|
||||
k = event.key
|
||||
now = pygame.time.get_ticks()
|
||||
|
||||
is_don_key = (k == pygame.K_f or k == pygame.K_j)
|
||||
is_ka_key = (k == pygame.K_d or k == pygame.K_k)
|
||||
|
||||
if is_don_key or is_ka_key:
|
||||
# Add to buffer
|
||||
self.key_buffer[k] = now
|
||||
|
||||
# Check for big note conditions
|
||||
# Don (F+J)
|
||||
f_down = (now - self.key_buffer.get(pygame.K_f, 0) < 50)
|
||||
j_down = (now - self.key_buffer.get(pygame.K_j, 0) < 50)
|
||||
|
||||
if f_down and j_down:
|
||||
self.editor.record_note(3) # Big Don
|
||||
# Clear buffer to prevent double trigger
|
||||
self.key_buffer[pygame.K_f] = 0
|
||||
self.key_buffer[pygame.K_j] = 0
|
||||
return
|
||||
|
||||
# Ka (D+K)
|
||||
d_down = (now - self.key_buffer.get(pygame.K_d, 0) < 50)
|
||||
k_down = (now - self.key_buffer.get(pygame.K_k, 0) < 50)
|
||||
|
||||
if d_down and k_down:
|
||||
self.editor.record_note(4) # Big Ka
|
||||
self.key_buffer[pygame.K_d] = 0
|
||||
self.key_buffer[pygame.K_k] = 0
|
||||
return
|
||||
|
||||
# Single Note (Small)
|
||||
# We need to wait a bit to ensure it's not a big note?
|
||||
# For real-time feedback, usually we trigger small immediately,
|
||||
# and upgrade to big if simultaneous? (Hard to upgrade)
|
||||
# Or just trigger small if the other key isn't down.
|
||||
# Simplified: If pressed, and other key NOT pressed recently, add small.
|
||||
|
||||
if is_don_key:
|
||||
# Check if partner is pressed
|
||||
partner = pygame.K_j if k == pygame.K_f else pygame.K_f
|
||||
if now - self.key_buffer.get(partner, 0) > 50:
|
||||
self.editor.record_note(1) # Small Don
|
||||
|
||||
if is_ka_key:
|
||||
partner = pygame.K_k if k == pygame.K_d else pygame.K_d
|
||||
if now - self.key_buffer.get(partner, 0) > 50:
|
||||
self.editor.record_note(2) # Small Ka
|
||||
|
||||
# Check regions
|
||||
if self.rect_header.collidepoint(mx, my):
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
self.input_active = None # Clear input focus
|
||||
# Use stored rects for precise hit testing
|
||||
if hasattr(self, 'tab_rects'):
|
||||
for d, rect in self.tab_rects.items():
|
||||
if rect.collidepoint(mx, my):
|
||||
self.editor.current_course_name = d
|
||||
break
|
||||
|
||||
# Export
|
||||
if hasattr(self, 'rect_exp') and self.rect_exp.collidepoint(mx, my):
|
||||
self.editor.save()
|
||||
print("Exported")
|
||||
|
||||
elif self.rect_preview.collidepoint(mx, my):
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
self.input_active = None
|
||||
# Toggle play
|
||||
self.editor.toggle_play()
|
||||
|
||||
elif self.rect_prop.collidepoint(mx, my):
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
# Check property clicks
|
||||
clicked_prop = None
|
||||
if hasattr(self, 'prop_rects'):
|
||||
for k, rect in self.prop_rects.items():
|
||||
if rect.collidepoint(mx, my):
|
||||
clicked_prop = k
|
||||
break
|
||||
|
||||
if clicked_prop:
|
||||
self.input_active = clicked_prop
|
||||
# Init text
|
||||
val = self.editor.tja.headers.get(clicked_prop, '')
|
||||
self.input_text = str(val)
|
||||
else:
|
||||
self.input_active = None
|
||||
|
||||
elif self.rect_timeline.collidepoint(mx, my):
|
||||
self.input_active = None
|
||||
# Track Area
|
||||
track_y = self.rect_timeline.y + 30
|
||||
track_rect = pygame.Rect(self.rect_timeline.x + 100, track_y, self.rect_timeline.w - 100, 80)
|
||||
|
||||
if track_rect.collidepoint(mx, my):
|
||||
clicked_note = self.get_note_at(mx, my, track_rect)
|
||||
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
if clicked_note:
|
||||
if event.button == 1:
|
||||
self.editor.selected_notes = [clicked_note]
|
||||
self.dragging_note = {'type': 'move', 'note': clicked_note}
|
||||
elif event.button == 3:
|
||||
course = self.editor.get_current_course()
|
||||
if course and clicked_note in course.notes:
|
||||
course.notes.remove(clicked_note)
|
||||
else:
|
||||
if event.button == 1:
|
||||
self.is_dragging_timeline = True
|
||||
self.last_mouse_x = mx
|
||||
|
||||
if event.type == pygame.MOUSEBUTTONUP:
|
||||
if self.dragging_note and self.dragging_note['type'] != 'move':
|
||||
# Drop from library
|
||||
time = self.x_to_time(mx, track_rect.x, track_rect.w)
|
||||
# Snap
|
||||
bpm = self.editor.tja.headers.get('BPM', 120)
|
||||
snap = (60000/bpm) / 4
|
||||
time = round(time/snap) * snap
|
||||
|
||||
beat_total = time / (60000/bpm)
|
||||
measure = int(beat_total / 4)
|
||||
beat = beat_total % 4
|
||||
|
||||
self.editor.add_note(self.dragging_note['type'], time, measure, beat)
|
||||
|
||||
self.dragging_note = None
|
||||
self.is_dragging_timeline = False
|
||||
|
||||
if event.type == pygame.MOUSEMOTION:
|
||||
if self.is_dragging_timeline:
|
||||
dx = mx - self.last_mouse_x
|
||||
self.editor.current_time -= dx / self.editor.zoom_x
|
||||
self.last_mouse_x = mx
|
||||
|
||||
if self.dragging_note and self.dragging_note.get('type') == 'move':
|
||||
time = self.x_to_time(mx, track_rect.x, track_rect.w)
|
||||
bpm = self.editor.tja.headers.get('BPM', 120)
|
||||
snap = (60000/bpm) / 4
|
||||
time = round(time/snap) * snap
|
||||
self.dragging_note['note'].time = time
|
||||
|
||||
if event.type == pygame.MOUSEWHEEL:
|
||||
if pygame.key.get_pressed()[pygame.K_LCTRL]:
|
||||
self.editor.zoom_x *= (1.1 if event.y > 0 else 0.9)
|
||||
else:
|
||||
self.editor.current_time -= event.y * 100
|
||||
|
||||
def get_note_at(self, mx, my, track_rect):
|
||||
course = self.editor.get_current_course()
|
||||
if not course: return None
|
||||
|
||||
cy = track_rect.centery
|
||||
for note in course.notes:
|
||||
nx = self.time_to_x(note.time, track_rect.x, track_rect.w)
|
||||
dist = ((mx - nx)**2 + (my - cy)**2)**0.5
|
||||
r = 24 if note.type in [3, 4] else 16
|
||||
if dist < r:
|
||||
return note
|
||||
return None
|
||||
Reference in New Issue
Block a user