133 lines
6.2 KiB
Markdown
133 lines
6.2 KiB
Markdown
# 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 });
|
||
```
|
||
|
||
Vì `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` và `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)
|