Files

133 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fix: MAIN SESSION & SECTION-TAB Mouse Behavior (MOUSE.md)
## Mô tả
Kiểm tra và sửa các lỗi trong MAIN SESSION và SECTION-TAB theo đặc tả [MOUSE.md](file:///home/locpham/SonicForgeStudio/md/MOUSE.md):
| Hành động | Đặc tả MOUSE.md |
|---|---|
| Click item | select item, drag to move |
| Ctrl+Click item | toggle selection (add/remove from group) |
| Ctrl+Click empty | deselect all |
| Ctrl+Click+Drag item/group | **copy** item/group to new track |
| Ctrl+Click+Drag empty | marquee selection/deselection |
---
## Bugs đã tìm thấy
### Bug 1 `handleSetPendingDrag`: luôn thêm `itemId` vào `ids`, bất kể đã toggle hay deselect
**Vị trí**: [app.jsx L11834-11837](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11834-L11837)
```js
const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e, preToggleSnapshot) => {
var ids = preToggleSnapshot || new Set(selectedItemIds);
ids.add(itemId); // ← luôn ADD itemId, bất kể đây là lần toggle-off
pendingDragRef.current = { ... };
};
```
- Khi user **Ctrl+Click** vào một item đang được selected → item đó bị deselect (line 1107).
- Nhưng `preToggleSnapshot` (được build **trước** toggle tại line 1111/1178) luôn được `ids.add(itemId)`, nên pendingDrag vẫn include item đó trong nhóm copy.
- **Kết quả**: Ctrl+Drag sau deselect vẫn copy item bị deselect → số lượng copies bị lỗi.
**Fix**: Khi pendingDrag được set sau toggle, nếu item bị deselect (tức `preToggleSnapshot.has(itemId)`) thì **xóa** `itemId` khỏi ids thay vì add.
Caller (line 1111) truyền `preToggleSnapshot` (snapshot TRƯỚC toggle), nên logic cần:
```js
// Nếu item đang có trong snapshot (tức là toggle-off / deselect), xóa khỏi ids
// Nếu item không có trong snapshot (tức là toggle-on / add), thêm vào ids
if (preToggleSnapshot && preToggleSnapshot.has(itemId)) {
ids.delete(itemId);
} else {
ids.add(itemId);
}
```
---
### Bug 2 `handleSectionItemDragStart` với `isDuplicate=true`: `newOriginals` bị build bên trong callback async của `updateActiveTracks`
**Vị trí**: [app.jsx L11888-11924](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11888-L11924)
```js
var newOriginals = {};
updateActiveTracks(function(prev) {
return prev.map(function(t) {
// ... build newOriginals ở đây (bên trong callback)
newOriginals[newId] = { ... };
return { ...t, ... };
});
});
// Sử dụng newOriginals NGAY SAU updateActiveTracks (có thể chưa được fill)
var newItemId = Object.keys(newOriginals)[0] || itemId;
setDraggedSectionItem({ ..., itemId: newItemId, multiIds: newOriginals });
```
`updateActiveTracks` gọi setState (React batch), callback của nó được gọi đồng bộ (React's setState updater function chạy đồng bộ). Nhưng vì `newOriginals` được closure-captured bên trong callback, điều này **thực ra hoạt động đúng** trong React 18 với batching.
Tuy nhiên: nếu **nhiều items có cùng loại** (section + midiItem + clip), vòng lặp qua `Object.keys(multiIds)` gọi `Date.now()` nhiều lần trong cùng một tick → có thể cho **cùng một newId** cho các items khác nhau, dẫn đến items bị merge.
**Fix**: Thêm index counter vào ID tạo để đảm bảo unique:
```js
var dupCounter = 0;
var newId = 'sec_dup_' + Date.now() + '_' + (dupCounter++) + '_' + Math.random()...
```
---
### Bug 3 Section-only path trong `isDuplicate` (non-multi): không xử lý `clip` type
**Vị trí**: [app.jsx L11925-11938](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L11925-L11938)
Khi không có `multiIds` (chỉ 1 item đơn lẻ), code chỉ handle `section``midiItem`:
```js
var items = itemType === 'section' ? (track.sections || []) : (track.midiItems || []);
```
Nếu `itemType === 'clip'`, `items` sẽ dùng `midiItems` (sai). Cần thêm case cho `clip`.
---
### Bug 4 Ctrl+Click trên CLIP ở line 1172: điều kiện kiểm tra bỏ qua hitItem
**Vị trí**: [app.jsx L1172](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx#L1172)
```js
if (clickedClip && (e.altKey || e.ctrlKey)) { ... }
```
Đoạn này xử lý Ctrl+Click trên **audio clip**. Tuy nhiên ở trên (line 1101), block `if (hitItem && !e.altKey && !e.shiftKey)` đã `return` nếu hitItem (section/midiItem) được click với Ctrl. Nhưng nếu không có hitItem và không có clickedClip, flow đúng (sweep select). Không có lỗi ở đây.
---
### Bug 5 `handleSetPendingDrag` cho Clip (line 1184): cùng vấn đề như Bug 1
Tương tự Bug 1, khi Ctrl+Click trên clip đang selected (sẽ bị deselect ở line 1180), nhưng `clipPreToggleSnapshot.has(clipCanonicalId)` là true và `ids.add(clipCanonicalId)` vẫn được thực thi trong `handleSetPendingDrag`.
---
## Proposed Changes
### [MODIFY] [app.jsx](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx)
#### Fix Bug 1 & 5 `handleSetPendingDrag` (L11834-11838)
Sửa logic toggle để đúng: nếu item **có** trong preToggleSnapshot → nó vừa bị deselect → **xóa** khỏi ids. Nếu không có → vừa được add → **thêm** vào ids.
#### Fix Bug 2 ID uniqueness trong multi-copy loop
Thêm counter index vào ID generation trong vòng lặp `Object.keys(multiIds).forEach`.
#### Fix Bug 3 Single-item copy: xử lý `clip` type
Trong nhánh `else` (non-multiIds) của `isDuplicate`, thêm xử lý `clip` type.
---
## Verification Plan
### Manual Verification
1. Mở MAIN SESSION
2. Thêm vài sections/MIDI items trên nhiều tracks
3. **Test Ctrl+Click toggle**: Click vào item A → selected. Ctrl+Click A → deselected. Ctrl+Click B → B selected (không phải A+B).
4. **Test Ctrl+Drag single**: Ctrl+Click item → drag → chỉ copy 1 item, không thêm item nào bị deselect
5. **Test Ctrl+Drag group**: Ctrl+Click A, Ctrl+Click B (2 selected) → drag từ A → copy cả A và B, không thêm C hay D
6. **Test Ctrl+Click empty**: Click trống → deselect all
7. **Test Ctrl+Drag empty**: Drag trên empty → marquee select
8. Lặp lại trên SECTION-TAB (session_xxx)