diff --git a/README.md b/README.md
index 197f009..aafac5d 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ xdg-mime query default application/x-nlve-project
| Удалить выделенное | Delete |
| Выделить всё | Ctrl+A |
| Выделить областью | протяжка по пустому месту дорожек |
+| Режим выделения: рамкой или по времени | D |
| Схлопнуть дыру под курсором | Ctrl+Shift+R |
| Отменить, повторить | Ctrl+Z, Ctrl+Y |
| Масштаб таймлайна | Ctrl + колесо мыши |
@@ -272,6 +273,32 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
удаляет: она лежит, пока проект не сохранят заново. После сохранения
копия не нужна и убирается.
+## Два режима выделения
+
+Кнопка внизу (справа от схлопывания) и клавиша `D`. Режим показан
+значком: рамка — выделение клипов, промежуток со стрелкой — выделение по
+времени.
+
+**Рамкой по клипам.** Протяжка по пустому месту дорожек выделяет всё, что
+рамка задела. Обычное выделение: выделил, нажал Delete.
+
+**По времени.** Протяжка отмечает промежуток от и до — по всем дорожкам
+сразу, сколько бы их ни было. Промежуток виден пунктиром и после того,
+как кнопку отпустили: по нему режет `S`.
+
+Зачем второй режим: вырезать из фильма кусок целиком. Отметил промежуток,
+нажал `S` — все дорожки разрезаны в двух местах, по краям промежутка, и
+выделенной осталась ровно середина. Дальше Delete, и при включённом
+автосхлопывании дыра закрывается сама.
+
+Края промежутка кладутся на кадровую сетку: между кадрами времени нет.
+
+**Курсор идёт за резом.** После `S` он встаёт ровно на место реза (в
+режиме по времени — на начало промежутка), после удаления куска и
+схлопывания — на место стыка. Дальше с этим местом и работают: следующий
+рез, схлопывание дыры, просмотр перехода. Раньше курсор оставался там, где
+был, и в место реза приходилось попадать мышью заново.
+
## Починка проекта
Меню «Проект» → **Проверить и починить проект...**. Показывает, что нашла,
diff --git a/icons_online/attribution.txt b/icons_online/attribution.txt
index 0f3b577..1586d15 100644
--- a/icons_online/attribution.txt
+++ b/icons_online/attribution.txt
@@ -2,4 +2,6 @@ Log panel: Prop
Ripple gaps: Merge Vertical icon by Icons8
Magnet: Magnet icon by Icons8
Playhead snapping: Клей icon by Icons8
-App icon: Showing Video Frames icon by Icons8
\ No newline at end of file
+App icon: Showing Video Frames icon by Icons8
+Selection mode, clips: Select All icon by Icons8
+Selection mode, time range: Width icon by Icons8
diff --git a/icons_online/icons8-select-all-90.png b/icons_online/icons8-select-all-90.png
new file mode 100644
index 0000000..0bfdc90
Binary files /dev/null and b/icons_online/icons8-select-all-90.png differ
diff --git a/icons_online/icons8-width-90.png b/icons_online/icons8-width-90.png
new file mode 100644
index 0000000..80a187b
Binary files /dev/null and b/icons_online/icons8-width-90.png differ
diff --git a/src/uIconData.pas b/src/uIconData.pas
index 2487924..34a8ee4 100644
--- a/src/uIconData.pas
+++ b/src/uIconData.pas
@@ -32,8 +32,8 @@ const
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
- $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
- $2C, $48, $09, $00, $00, $01, $3E, $49, $44, $41, $54, $38, $CB, $BD, $92, $31,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $3E, $49, $44, $41, $54, $38, $CB, $BD, $92, $31,
$4B, $C3, $40, $14, $C7, $7F, $97, $44, $29, $68, $03, $5A, $45, $EA, $D0, $A5,
$6B, $41, $44, $5C, $0A, $0E, $3A, $15, $DC, $1D, $DD, $FB, $51, $74, $71, $29,
$F8, $39, $EC, $62, $06, $17, $5D, $EA, $22, $D2, $88, $93, $E0, $22, $68, $A1,
@@ -66,8 +66,8 @@ const
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
- $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
- $2C, $48, $09, $00, $00, $01, $2F, $49, $44, $41, $54, $38, $CB, $CD, $D2, $3D,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $2F, $49, $44, $41, $54, $38, $CB, $CD, $D2, $3D,
$2F, $83, $51, $18, $C6, $F1, $DF, $C3, $63, $31, $34, $11, $B5, $35, $D1, $A9,
$C1, $E0, $25, $4C, $66, $49, $F9, $16, $24, $96, $C6, $37, $A8, $18, $7D, $02,
$31, $98, $18, $99, $25, $44, $F8, $02, $26, $B1, $30, $30, $78, $D9, $90, $18,
@@ -99,8 +99,8 @@ const
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
- $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
- $2C, $48, $09, $00, $00, $01, $0F, $49, $44, $41, $54, $38, $CB, $B5, $D3, $BF,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $0F, $49, $44, $41, $54, $38, $CB, $B5, $D3, $BF,
$2E, $83, $51, $18, $C7, $F1, $CF, $5B, $15, $0C, $74, $93, $88, $91, $0B, $E8,
$68, $60, $33, $34, $B1, $1A, $F4, $1E, $0C, $66, $8B, $C5, $05, $B8, $01, $89,
$2B, $30, $18, $24, $4C, $0D, $8B, $41, $13, $46, $62, $90, $18, $A4, $4B, $55,
@@ -130,8 +130,8 @@ const
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
- $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
- $2C, $48, $09, $00, $00, $01, $67, $49, $44, $41, $54, $38, $CB, $B5, $D3, $3D,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $67, $49, $44, $41, $54, $38, $CB, $B5, $D3, $3D,
$4F, $93, $61, $14, $C6, $F1, $5F, $AB, $80, $9D, $A8, $A1, $71, $95, $08, $14,
$3F, $80, $71, $D2, $8D, $C5, $D8, $A4, $A6, $61, $31, $74, $32, $31, $1A, $99,
$89, $B3, $8B, $71, $B0, $7C, $0F, $37, $42, $4B, $07, $07, $D0, $98, $30, $E0,
@@ -167,8 +167,8 @@ const
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
- $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
- $2C, $48, $09, $00, $00, $02, $71, $49, $44, $41, $54, $38, $CB, $95, $D4, $41,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $02, $71, $49, $44, $41, $54, $38, $CB, $95, $D4, $41,
$68, $DB, $65, $18, $06, $F0, $5F, $E2, $DA, $2A, $2E, $59, $B2, $65, $29, $4B,
$B4, $A7, $C9, $B0, $DB, $A0, $8C, $49, $63, $55, $50, $0F, $56, $31, $ED, $68,
$BA, $CB, $70, $3B, $C8, $F0, $24, $08, $0A, $1E, $44, $99, $4E, $AA, $B2, $5D,
@@ -211,6 +211,76 @@ const
$00, $00, $00, $00, $49, $45, $4E, $44, $AE, $42, $60, $82
);
+ { icons8-select-all-90.png, 24 на 24 точек }
+ IconSelectClipsData: array[0..509] of Byte = (
+ $89, $50, $4E, $47, $0D, $0A, $1A, $0A, $00, $00, $00, $0D, $49, $48, $44, $52,
+ $00, $00, $00, $18, $00, $00, $00, $18, $08, $04, $00, $00, $00, $4A, $7E, $F5,
+ $73, $00, $00, $00, $20, $63, $48, $52, $4D, $00, $00, $7A, $26, $00, $00, $80,
+ $84, $00, $00, $FA, $00, $00, $00, $80, $E8, $00, $00, $75, $30, $00, $00, $EA,
+ $60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
+ $02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
+ $48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $63, $49, $44, $41, $54, $38, $CB, $B5, $D3, $BD,
+ $6E, $13, $41, $14, $05, $E0, $6F, $76, $13, $7B, $17, $21, $A4, $98, $48, $34,
+ $3C, $00, $05, $0A, $52, $78, $05, $AA, $50, $24, $8F, $C1, $1B, $E4, $1D, $D2,
+ $51, $41, $49, $43, $EB, $CE, $A9, $90, $A5, $28, $D6, $26, $15, $55, $8A, $3C,
+ $00, $42, $32, $08, $2A, $62, $25, $CE, $7A, $87, $C2, $E3, $3F, $61, $0A, $17,
+ $39, $53, $CC, $E8, $9E, $39, $33, $F7, $DC, $99, $CB, $43, $23, $A4, $79, $CB,
+ $36, $EE, $4D, $B4, $05, $DC, $A1, $8D, $E8, $4E, $6E, $1B, $63, $93, $85, $2C,
+ $77, $EC, $5C, $E5, $C8, $AE, $AE, $4A, $DF, $9E, $3D, $7D, $95, $AE, $5D, $47,
+ $2A, $E7, $8E, $E5, $90, $81, $96, $03, $FB, $32, $71, $6D, $16, $51, $66, $DF,
+ $81, $D6, $22, $54, $1A, $B8, $B0, $23, $17, $14, $4A, $A5, $4C, $A6, $54, $2A,
+ $04, $B9, $1D, $17, $06, $CA, $69, $32, $53, $FC, $34, $70, $65, $82, $5A, $AD,
+ $16, $C5, $B4, $22, $AA, $FD, $50, $B9, $D6, $CC, $4C, $07, $4F, $F1, $4B, $14,
+ $74, $3C, $9E, $A7, $16, $FC, $F1, $5B, $5C, $E2, $13, $0A, $5D, $5D, $05, $3A,
+ $4E, $0D, $7D, $4F, $63, $E8, $54, $67, $85, $B7, $95, $4E, $7A, $96, $4A, $7C,
+ $EB, $8B, $6F, $9A, $14, $0D, $AE, $DD, $AE, $F0, $49, $30, $AD, $3B, $8C, $BC,
+ $9F, $C7, $EE, $93, $70, $99, $4F, $1E, $32, $2F, $71, $A5, $F1, $C4, $89, $57,
+ $1A, $44, $1F, $7C, $4E, $BB, $16, $7C, $7A, $87, $8D, $51, $EA, $EB, $4F, $EB,
+ $2C, $D7, $4E, $23, $5B, $C7, $CF, $F2, $6D, $A7, $F9, $91, $77, $5E, $AC, $98,
+ $FE, $68, $B4, $C4, $27, $41, $34, $44, $44, $E1, $8D, $D7, $F3, $6F, $96, $7B,
+ $EE, $93, $D1, $12, $BF, $F9, $C3, $85, $74, $CF, $5B, $F4, $D4, $FF, $F1, $F8,
+ $0F, $BF, $C1, $E7, $9B, $09, $CE, $DC, $B8, $74, $B8, $B6, $1F, $0E, $5D, $BA,
+ $71, $B6, $5C, $A5, $B1, $9E, $4C, $3E, $EF, $BF, $55, $04, $8D, $AF, $7A, $C6,
+ $0B, $D3, $E4, $5A, $36, $68, $D1, $87, $C4, $5F, $C8, $81, $83, $A2, $85, $86,
+ $DA, $DD, $00, $00, $00, $00, $49, $45, $4E, $44, $AE, $42, $60, $82
+ );
+
+ { icons8-width-90.png, 24 на 24 точек }
+ IconSelectRangeData: array[0..473] of Byte = (
+ $89, $50, $4E, $47, $0D, $0A, $1A, $0A, $00, $00, $00, $0D, $49, $48, $44, $52,
+ $00, $00, $00, $18, $00, $00, $00, $18, $08, $04, $00, $00, $00, $4A, $7E, $F5,
+ $73, $00, $00, $00, $20, $63, $48, $52, $4D, $00, $00, $7A, $26, $00, $00, $80,
+ $84, $00, $00, $FA, $00, $00, $00, $80, $E8, $00, $00, $75, $30, $00, $00, $EA,
+ $60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
+ $02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
+ $48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
+ $00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $09, $1D, $14, $21, $19, $27,
+ $B3, $C9, $F8, $00, $00, $01, $3F, $49, $44, $41, $54, $38, $CB, $CD, $D2, $BF,
+ $4A, $1C, $51, $18, $05, $F0, $DF, $8C, $3B, $8E, $53, $64, $70, $94, $2D, $5D,
+ $B0, $90, $B0, $C5, $DA, $A6, $14, $C1, $28, $E8, $1B, $F8, $0C, $61, $FB, $7D,
+ $02, $5F, $40, $D8, $07, $91, $6D, $92, $52, $2D, $6C, $BD, $45, $08, $29, $84,
+ $ED, $C4, $C2, $90, $26, $8C, $6E, $66, $53, $B8, $7F, $26, $82, $85, $0A, $E2,
+ $B9, $C5, $2D, $CE, $3D, $DF, $F7, $9D, $F3, $5D, $DE, $1D, $16, $26, $77, $2E,
+ $55, $CA, $7C, $50, $4A, $14, $4A, $91, $15, $23, $63, $85, $C8, $68, $C2, $CF,
+ $D0, $D0, $D7, $17, $EB, $3A, $91, $DB, $73, $66, $C3, $86, $33, $7B, $72, $27,
+ $BA, $62, $7D, $7D, $8D, $87, $A7, $90, $D8, $44, $22, $A0, $34, $34, $F0, $0B,
+ $03, $43, $A5, $6F, $C2, $8C, $1F, $4D, $3B, $64, $CE, $9D, $CB, $9E, $1C, $BC,
+ $C6, $C7, $CF, $35, $FD, $2A, $C1, $58, $C7, $BE, $C5, $FF, $F8, $45, $FB, $3A,
+ $C6, $F5, $7C, $A6, $A8, $7C, $72, $AC, $12, $DC, $88, $66, $25, $9A, $8E, $C4,
+ $BE, $A8, $A6, $A5, $A3, $89, $A9, $AF, $D6, $FD, $D6, $76, $EB, $87, $BF, $35,
+ $C1, $82, $8F, $0A, $DF, $E5, $AE, $EC, $FA, $F3, $02, $0F, $F3, $D8, $4E, $6D,
+ $09, $2E, $AD, $59, $92, $4D, $CE, $92, $35, $97, $82, $2D, $A7, $D3, $58, $E7,
+ $1E, $62, $17, $0E, $B5, $5C, $BB, $AB, $95, $BA, $D6, $33, $F4, $73, $3E, $49,
+ $A3, $46, $46, $82, $F0, $A8, $F7, $9D, $01, $F5, $95, $BE, $ED, $E2, $2A, $DB,
+ $BA, $52, $6D, $3D, $4D, $4D, $3D, $6D, $A9, $AE, $6D, $D5, $63, $C1, $48, $10,
+ $DC, $EB, $D8, $91, $6A, $39, $B0, $6C, $D9, $81, $96, $D4, $67, $1D, $F7, $82,
+ $30, $FF, $AB, $90, $CB, $91, $29, $44, $12, $AB, $62, $B1, $55, $89, $48, $21,
+ $9B, $F1, $EF, $13, $FF, $00, $8F, $00, $4E, $D0, $9E, $28, $A1, $C0, $00, $00,
+ $00, $00, $49, $45, $4E, $44, $AE, $42, $60, $82
+ );
+
{ icons8-показ-видеокадров-30.png, значок окна, 32 на 32 точек, формат ico }
IconAppData: array[0..4285] of Byte = (
$00, $00, $01, $00, $01, $00, $20, $20, $00, $00, $01, $00, $20, $00, $A8, $10,
diff --git a/src/uMainForm.lfm b/src/uMainForm.lfm
index f9902ae..578948e 100644
--- a/src/uMainForm.lfm
+++ b/src/uMainForm.lfm
@@ -97,11 +97,21 @@ object MainForm: TMainForm
OnClick = btnRippleMenuClick
TabStop = False
end
+ object btnSelectMode: TThemedButton
+ Left = 448
+ Height = 28
+ Top = 6
+ Width = 28
+ Icon = tiSelectClips
+ OnClick = btnSelectModeClick
+ ShowHint = True
+ TabStop = False
+ end
object lblStatus: TLabel
- Left = 488
+ Left = 518
Height = 20
Top = 10
- Width = 780
+ Width = 750
Anchors = [akTop, akLeft, akRight]
AutoSize = False
Caption = 'Готов.'
diff --git a/src/uMainForm.pas b/src/uMainForm.pas
index 7e6856c..06a6ab7 100644
--- a/src/uMainForm.pas
+++ b/src/uMainForm.pas
@@ -51,6 +51,7 @@ type
btnCenter: TThemedButton;
btnRipple: TThemedButton;
btnRippleMenu: TThemedButton;
+ btnSelectMode: TThemedButton;
lblPoolInfo: TLabel;
lblStatus: TLabel;
lblTime: TLabel;
@@ -72,6 +73,7 @@ type
procedure btnCenterClick(Sender: TObject);
procedure btnRippleClick(Sender: TObject);
procedure btnRippleMenuClick(Sender: TObject);
+ procedure btnSelectModeClick(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
@@ -156,6 +158,7 @@ type
{ Пункт «Ключ и прозрачность в предпросмотре»: галку в нём приходится
держать за руку — она не следует за настройкой сама. }
FKeyPreviewItem: TMenuItem;
+ FSelectModeItem: TMenuItem;
{ Показанная доля выполнения. Отдельно от настоящей, потому что растёт
она по своим правилам — см. AdvanceJobBar. }
FJobShown: Double;
@@ -240,6 +243,8 @@ type
procedure TrackMenuDown(Sender: TObject);
procedure UpdateSnapButton;
procedure UpdateGlueButton;
+ procedure UpdateSelectModeButton;
+ procedure TimelineSelectModeChange(Sender: TObject);
procedure UpdateCenterButton;
procedure ToggleCenterPlayhead;
procedure RippleModeChosen(Sender: TObject);
@@ -372,6 +377,7 @@ type
{ Что и откуда собирать: с учётом отмеченного участка. }
procedure DescribeRegion(out AFrom, ATo: Double);
procedure MenuSelectAll(Sender: TObject);
+ procedure MenuSelectMode(Sender: TObject);
procedure MenuRipple(Sender: TObject);
procedure MenuToggleRipple(Sender: TObject);
procedure MenuGoStart(Sender: TObject);
@@ -525,6 +531,7 @@ begin
UpdateRippleButton;
UpdateSnapButton;
UpdateGlueButton;
+ UpdateSelectModeButton;
UpdateCenterButton;
UpdateCaption;
SetStatus('Готов. Добавьте файлы в проект.');
@@ -662,6 +669,7 @@ begin
FTimeline.OnTrackMenu := @TimelineTrackMenu;
FTimeline.OnTrackRename := @TimelineTrackRename;
FTimeline.OnRegionChange := @TimelineRegionChange;
+ FTimeline.OnSelectionModeChange := @TimelineSelectModeChange;
FTimeline.OnActiveTrackChange := @TimelineActiveTrackChange;
end;
@@ -768,6 +776,10 @@ begin
AddItem(MEdit, 'Разрезать по курсору', @MenuSplit, ShortCut(VK_S, []));
AddItem(MEdit, 'Удалить выделенное', @MenuDelete, ShortCut(VK_DELETE, []));
AddItem(MEdit, 'Выделить всё', @MenuSelectAll, ShortCut(VK_A, [ssCtrl]));
+ { Пункт держит и подпись режима: по одному значку на кнопке не угадать,
+ что сейчас делает протяжка мышью. }
+ FSelectModeItem := AddItem(MEdit, 'Режим выделения', @MenuSelectMode,
+ ShortCut(VK_D, []));
AddItem(MEdit, 'Цветовой ключ клипа...', @MenuColorKey);
AddSep(MEdit);
AddItem(MEdit, 'Копировать клипы', @MenuCopyClips, ShortCut(VK_C, [ssCtrl]));
@@ -900,7 +912,9 @@ procedure TMainForm.BuildJobIndicator;
const
JobWidth = 330;
LogButtonWidth = 28;
- LogButtonLeft = 448;
+ { Журнал стоит после переключателя выделения: сперва то, что меняет
+ работу с монтажом, потом то, что показывает сообщения. }
+ LogButtonLeft = 478;
begin
FJobLabel := TLabel.Create(Self);
FJobLabel.Parent := pnlBottom;
@@ -2575,6 +2589,60 @@ begin
UpdateGlueButton;
end;
+{ Кнопка режима выделения.
+
+ Режима два, и кнопка показывает не «включено-выключено», а какой сейчас:
+ значок рамки — выделение клипов, значок промежутка — выделение по
+ времени. Поэтому она не утапливается, а меняет картинку. }
+procedure TMainForm.UpdateSelectModeButton;
+begin
+ if FSelectModeItem <> nil then
+ if FTimeline.SelectionMode = smRange then
+ FSelectModeItem.Caption := 'Режим выделения: по времени (сейчас)'
+ else
+ FSelectModeItem.Caption := 'Режим выделения: рамкой по клипам (сейчас)';
+
+ if FTimeline.SelectionMode = smRange then
+ begin
+ btnSelectMode.Icon := tiSelectRange;
+ btnSelectMode.Hint := 'Выделение по времени: протяжка отмечает ' +
+ 'промежуток по всем дорожкам.' + LineEnding +
+ 'S режет все дорожки по обоим краям промежутка.' + LineEnding +
+ 'Переключить (D) на выделение клипов рамкой.';
+ end
+ else
+ begin
+ btnSelectMode.Icon := tiSelectClips;
+ btnSelectMode.Hint := 'Выделение клипов: рамка выделяет всё, что ' +
+ 'задела.' + LineEnding +
+ 'S режет по курсору.' + LineEnding +
+ 'Переключить (D) на выделение промежутка по времени.';
+ end;
+end;
+
+procedure TMainForm.btnSelectModeClick(Sender: TObject);
+begin
+ FTimeline.ToggleSelectionMode;
+end;
+
+procedure TMainForm.MenuSelectMode(Sender: TObject);
+begin
+ FTimeline.ToggleSelectionMode;
+end;
+
+{ Режим сменили — сказать об этом словами: по одному значку не угадать,
+ что теперь делает протяжка мышью. }
+procedure TMainForm.TimelineSelectModeChange(Sender: TObject);
+begin
+ UpdateSelectModeButton;
+ if FTimeline.SelectionMode = smRange then
+ SetStatus('Выделение по времени: протяжка отмечает промежуток, ' +
+ 'S режет по его краям.', True)
+ else
+ SetStatus('Выделение клипов: рамка выделяет всё, что задела, ' +
+ 'S режет по курсору.', True);
+end;
+
{ Держать ли курсор посередине при воспроизведении.
Включено — монтаж едет под курсором, и всегда видно, что будет дальше.
diff --git a/src/uTheme.pas b/src/uTheme.pas
index 08c9e0e..f531cd9 100644
--- a/src/uTheme.pas
+++ b/src/uTheme.pas
@@ -70,7 +70,9 @@ type
tiRipple, // схлопывание: дыра между кусками закрывается
tiLog, // журнал: полоса с сообщениями
tiGlue, // курсор липнет к границам клипов
- tiTarget // курсор держится середины монтажа
+ tiTarget, // курсор держится середины монтажа
+ tiSelectClips, // выделение рамкой: что задело, то и выделено
+ tiSelectRange // выделение по времени: от и до, по всем дорожкам
);
{ Полоска выполнения, которая рисует себя сама.
@@ -460,6 +462,10 @@ begin
tiLog: Stream.WriteBuffer(IconLogData[0], Length(IconLogData));
tiGlue: Stream.WriteBuffer(IconGlueData[0], Length(IconGlueData));
tiTarget: Stream.WriteBuffer(IconTargetData[0], Length(IconTargetData));
+ tiSelectClips: Stream.WriteBuffer(IconSelectClipsData[0],
+ Length(IconSelectClipsData));
+ tiSelectRange: Stream.WriteBuffer(IconSelectRangeData[0],
+ Length(IconSelectRangeData));
else
Exit;
end;
diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas
index dd53de1..9f51c17 100644
--- a/src/uTimelineCtrl.pas
+++ b/src/uTimelineCtrl.pas
@@ -45,6 +45,16 @@ type
taRubberBand // обтягиваем рамкой клипы по пустому месту
);
+ { Как понимать протяжку мышью по пустому месту.
+
+ smClips — что рамка задела, то и выделено: привычное выделение
+ прямоугольником.
+
+ smRange — отмечается промежуток времени по всем дорожкам сразу, от и
+ до. Нужен, чтобы вырезать кусок фильма целиком: отметил промежуток,
+ нажал S — и все дорожки разрезаны в двух местах, по краям промежутка. }
+ TSelectionMode = (smClips, smRange);
+
{ Что находится под указателем в полосе заголовков дорожек. }
THeaderPart = (hpNone, hpBody, hpMute, hpSolo, hpUp, hpDown, hpName);
@@ -139,6 +149,14 @@ type
FBandToTime: Double;
FBandFromY: Integer;
FBandToY: Integer;
+ { Режим выделения и отмеченный им промежуток времени.
+
+ Промежуток живёт до следующего выделения или щелчка: по нему режет
+ клавиша S, и человек должен видеть, где именно это произойдёт. }
+ FSelectionMode: TSelectionMode;
+ FRangeFrom: Double;
+ FRangeTo: Double;
+ FOnSelectionModeChange: TNotifyEvent;
FGroupClips: array of TClip;
FGroupOrigStarts: array of Double;
@@ -251,6 +269,9 @@ type
const ATime, ATolerance: Double; out AFrom, ATo: Double): Boolean;
{ Допуск прилипания в секундах: те же экранные точки, что у краёв. }
function SnapTolerance: Double;
+ procedure SetSelectionMode(AValue: TSelectionMode);
+ { Рез в указанном месте. Возвращает, сколько клипов разрезано. }
+ function SplitAt(const ATime: Double; APrecise: Boolean): Integer;
protected
procedure Paint; override;
procedure DragOver(Source: TObject; X, Y: Integer; State: TDragState;
@@ -332,6 +353,22 @@ type
procedure SelectInRange(const AFrom, ATo: Double;
ATrackFrom, ATrackTo: Integer; AAdd: Boolean);
+ { Режим выделения: рамкой по клипам или промежутком времени по всем
+ дорожкам. Переключается клавишей D и кнопкой внизу. }
+ property SelectionMode: TSelectionMode read FSelectionMode
+ write SetSelectionMode;
+ procedure ToggleSelectionMode;
+ { Отмечен ли промежуток времени и какой. }
+ function HasSelectionRange: Boolean;
+ property RangeFrom: Double read FRangeFrom;
+ property RangeTo: Double read FRangeTo;
+ { Отметить промежуток. Мышь зовёт это же, и проверки тоже: так
+ поведение реза проверяется числами, без беготни указателем. }
+ procedure SetSelectionRange(const AFrom, ATo: Double);
+ procedure ClearSelectionRange;
+ property OnSelectionModeChange: TNotifyEvent read FOnSelectionModeChange
+ write FOnSelectionModeChange;
+
{ Номер дорожки под указателем мыши, минус один если ни одной. }
property HoverTrack: Integer read FHoverTrack;
{ Дорожка, выбранная щелчком по заголовку. }
@@ -1381,14 +1418,45 @@ end;
procedure TTimelineControl.DrawRubberBand(ACanvas: TCanvas);
var
L, R, T, B: Integer;
+ Pulling: Boolean;
begin
- if (FAction <> taRubberBand) or (not FMovedWhileDown) then
+ { Имя не Dragging: так называется метод самого элемента управления. }
+ Pulling := (FAction = taRubberBand) and FMovedWhileDown;
+
+ { Отмеченный промежуток виден и после протяжки: по нему режет клавиша S,
+ и человек должен знать, где это произойдёт. }
+ if (not Pulling) and HasSelectionRange then
+ begin
+ L := Max(TimeToX(FRangeFrom), FHeaderWidth);
+ R := Min(TimeToX(FRangeTo), ClientWidth);
+ if R <= L then
+ Exit;
+ ACanvas.Pen.Color := ColorBand;
+ ACanvas.Pen.Style := psDot;
+ ACanvas.Brush.Style := bsClear;
+ ACanvas.Rectangle(L, FRulerHeight, R, ClientHeight);
+ ACanvas.Pen.Style := psSolid;
+ Exit;
+ end;
+
+ if not Pulling then
Exit;
L := Min(TimeToX(FBandFromTime), TimeToX(FBandToTime));
R := Max(TimeToX(FBandFromTime), TimeToX(FBandToTime));
- T := Min(FBandFromY, FBandToY) - FScrollY;
- B := Max(FBandFromY, FBandToY) - FScrollY;
+
+ if FSelectionMode = smRange then
+ begin
+ { По времени отмечают промежуток, а не прямоугольник: рамка идёт во
+ всю высоту дорожек, сколько бы их ни было. }
+ T := FRulerHeight;
+ B := ClientHeight;
+ end
+ else
+ begin
+ T := Min(FBandFromY, FBandToY) - FScrollY;
+ B := Max(FBandFromY, FBandToY) - FScrollY;
+ end;
L := Max(L, FHeaderWidth);
R := Min(R, ClientWidth);
@@ -2618,10 +2686,22 @@ begin
begin
FAction := taNone;
if FMovedWhileDown then
- SelectInRange(FBandFromTime, FBandToTime,
- (FBandFromY - FRulerHeight) div FTrackHeight,
- (FBandToY - FRulerHeight) div FTrackHeight,
- ssCtrl in Shift)
+ begin
+ if FSelectionMode = smRange then
+ begin
+ { Промежуток времени: по всем дорожкам, от и до. Края кладутся на
+ кадровую сетку — по ним потом режет клавиша S. }
+ SetSelectionRange(FBandFromTime, FBandToTime);
+ if (FProject <> nil) and (FRangeTo > FRangeFrom) then
+ SelectInRange(FRangeFrom, FRangeTo, 0, FProject.TrackCount - 1,
+ ssCtrl in Shift);
+ end
+ else
+ SelectInRange(FBandFromTime, FBandToTime,
+ (FBandFromY - FRulerHeight) div FTrackHeight,
+ (FBandToY - FRulerHeight) div FTrackHeight,
+ ssCtrl in Shift);
+ end
else
begin
if (FProject <> nil) and not (ssCtrl in Shift) then
@@ -2630,6 +2710,8 @@ begin
if Assigned(FOnSelectionChange) then
FOnSelectionChange(Self);
end;
+ { Щелчок снимает и отметку промежутка: она про прошлое выделение. }
+ ClearSelectionRange;
Playhead := Max(0, SnapPlayhead(XToTime(X)));
end;
FMovedWhileDown := False;
@@ -2720,12 +2802,18 @@ end;
{ --- Правки --- }
-function TTimelineControl.SplitAtPlayhead(APrecise: Boolean): Integer;
+{ Рез в указанном месте.
+
+ Место кладётся на кадровую сетку: между кадрами времени нет, и рез на
+ доле кадра оставил бы в монтаже время, которого не бывает. Сдвиг не
+ больше полкадра. }
+function TTimelineControl.SplitAt(const ATime: Double;
+ APrecise: Boolean): Integer;
var
T: Integer;
Track: TTrack;
Clip, Tail: TClip;
- Offset: Double;
+ Offset, At: Double;
{ Старый номер связки — новый. Одна связка при резе становится двумя:
голова со своей парой и хвост со своей. }
NewGroups: TStringList;
@@ -2735,6 +2823,7 @@ begin
if FProject = nil then
Exit;
+ At := QuantizeTime(ATime);
FProject.BeginChange;
NewGroups := TStringList.Create;
try
@@ -2745,11 +2834,11 @@ begin
for T := 0 to FProject.TrackCount - 1 do
begin
Track := FProject.Tracks[T];
- Clip := Track.ClipAt(FPlayhead);
+ Clip := Track.ClipAt(At);
if Clip = nil then
Continue;
- Offset := FPlayhead - Clip.TlStart;
+ Offset := At - Clip.TlStart;
{ Рез вплотную к краю ничего не даёт, только плодит клипы нулевой длины. }
if (Offset < 0.001) or (Offset > Clip.Duration - 0.001) then
Continue;
@@ -2758,7 +2847,7 @@ begin
Tail.Id := Clip.Id + '_b';
Tail.MediaId := Clip.MediaId;
Tail.SrcIn := Clip.SrcIn + Offset;
- Tail.TlStart := FPlayhead;
+ Tail.TlStart := At;
Tail.Duration := Clip.Duration - Offset;
Tail.EdgeOut := Clip.EdgeOut;
{ Хвост — тот же клип, только вторая его половина. Всё настроенное
@@ -2819,6 +2908,90 @@ begin
FProject.Undo;
end;
+{ Режим выделения: рамкой по клипам или промежутком по времени.
+
+ Смена режима снимает отмеченный промежуток: он принадлежит режиму «по
+ времени», и оставлять его в другом режиме значило бы резать по невидимой
+ метке. }
+procedure TTimelineControl.SetSelectionMode(AValue: TSelectionMode);
+begin
+ if FSelectionMode = AValue then
+ Exit;
+ FSelectionMode := AValue;
+ ClearSelectionRange;
+ Invalidate;
+ if Assigned(FOnSelectionModeChange) then
+ FOnSelectionModeChange(Self);
+end;
+
+procedure TTimelineControl.ToggleSelectionMode;
+begin
+ if FSelectionMode = smClips then
+ SelectionMode := smRange
+ else
+ SelectionMode := smClips;
+end;
+
+function TTimelineControl.HasSelectionRange: Boolean;
+begin
+ Result := (FSelectionMode = smRange) and (FRangeTo - FRangeFrom > 0.0005);
+end;
+
+procedure TTimelineControl.SetSelectionRange(const AFrom, ATo: Double);
+begin
+ FRangeFrom := Max(0, QuantizeTime(Min(AFrom, ATo)));
+ FRangeTo := Max(0, QuantizeTime(Max(AFrom, ATo)));
+ Invalidate;
+end;
+
+procedure TTimelineControl.ClearSelectionRange;
+begin
+ if (FRangeFrom = 0) and (FRangeTo = 0) then
+ Exit;
+ FRangeFrom := 0;
+ FRangeTo := 0;
+ Invalidate;
+end;
+
+{ Рез по команде человека.
+
+ Обычно — по курсору. Если отмечен промежуток времени (режим выделения
+ «по времени»), режем по обоим его краям: это и есть «вырезать кусок
+ фильма» — два реза, между ними то, что уйдёт.
+
+ Курсор после реза встаёт ровно на место реза: дальше с этим местом и
+ работают — удаляют кусок, схлопывают дыру, ставят следующий рез. }
+function TTimelineControl.SplitAtPlayhead(APrecise: Boolean): Integer;
+var
+ At: Double;
+begin
+ Result := 0;
+ if FProject = nil then
+ Exit;
+
+ if HasSelectionRange then
+ begin
+ { Сперва дальний край: рез не двигает то, что левее, но порядок
+ всё равно понятнее от конца к началу. }
+ Result := SplitAt(FRangeTo, APrecise);
+ Result := Result + SplitAt(FRangeFrom, APrecise);
+ Playhead := Max(0, QuantizeTime(FRangeFrom));
+
+ { После реза выделенной остаётся ровно середина — то, что между
+ краями. Иначе Delete убрал бы не её: выделение осталось бы на
+ клипах, какими они были до реза, а середина оказалась бы новой,
+ невыделенной половиной. }
+ if Result > 0 then
+ SelectInRange(FRangeFrom, FRangeTo, 0, FProject.TrackCount - 1, False);
+ Exit;
+ end;
+
+ At := QuantizeTime(FPlayhead);
+ Result := SplitAt(At, APrecise);
+ if Result > 0 then
+ Playhead := Max(0, At);
+end;
+
{ Расширяет выделение на всю связку: клипы, связанные с уже выделенными,
тоже считаются выделенными. Иначе удаление или перенос разорвали бы
связку пополам. }
@@ -2974,7 +3147,7 @@ var
T, C: Integer;
Track: TTrack;
AnyChange: Boolean;
- HoleEnd: Double;
+ HoleEnd, HoleStart: Double;
Touched: TStringList;
begin
if FProject = nil then
@@ -2986,6 +3159,7 @@ begin
AnyChange := False;
HoleEnd := 0;
+ HoleStart := -1;
FProject.BeginChange;
Touched := TStringList.Create;
try
@@ -2997,6 +3171,9 @@ begin
begin
{ Правый край дыры: дальше него всё поедет влево. }
HoleEnd := Max(HoleEnd, Track[C].TlEnd);
+ { Левый край — место будущего стыка: туда встанет курсор. }
+ if (HoleStart < 0) or (Track[C].TlStart < HoleStart) then
+ HoleStart := Track[C].TlStart;
if Touched.IndexOf(IntToStr(T)) < 0 then
Touched.Add(IntToStr(T));
Track.Remove(Track[C]);
@@ -3014,7 +3191,17 @@ begin
end;
if AnyChange then
- StructureChanged
+ begin
+ { Курсор — на место, где был вырезанный кусок: при схлопывании там
+ теперь стык, без схлопывания — начало дыры. С этим местом и работают
+ дальше, и искать его мышью заново незачем. }
+ if HoleStart >= 0 then
+ Playhead := Max(0, HoleStart);
+ { Отметка промежутка пережила бы удаление и резала бы потом по
+ пустому месту. }
+ ClearSelectionRange;
+ StructureChanged;
+ end
else
FProject.Undo;
end;
@@ -3095,6 +3282,9 @@ begin
if AnyChange then
begin
+ { Курсор — на стык: дыры больше нет, а работать дальше будут именно с
+ этим местом. }
+ Playhead := Max(0, GapStart);
StructureChanged;
Result := True;
end
@@ -3120,7 +3310,7 @@ procedure TTimelineControl.SelectInRange(const AFrom, ATo: Double;
ATrackFrom, ATrackTo: Integer; AAdd: Boolean);
var
T, C, T1, T2: Integer;
- From_, To_: Double;
+ From_, To_, Eps: Double;
Clip: TClip;
begin
if FProject = nil then
@@ -3129,6 +3319,14 @@ begin
{ Рамку тянут в любую сторону — приводим её к возрастающему виду. }
From_ := Min(AFrom, ATo);
To_ := Max(AFrom, ATo);
+
+ { Нужно настоящее перекрытие, а не касание краями. Сосед, кончающийся
+ ровно там, где рамка начинается, в неё не попал — но сложение долей
+ даёт разницу в последнем знаке, и «больше» срабатывает. Порог —
+ полкадра: перекрытия короче кадра не бывает. }
+ Eps := 0.0005;
+ if (FProject.Master.FpsNum > 0) and (FProject.Master.FpsDen > 0) then
+ Eps := FProject.Master.FpsDen / FProject.Master.FpsNum / 2;
T1 := Max(0, Min(ATrackFrom, ATrackTo));
T2 := Min(FProject.TrackCount - 1, Max(ATrackFrom, ATrackTo));
@@ -3139,7 +3337,7 @@ begin
for C := 0 to FProject.Tracks[T].Count - 1 do
begin
Clip := FProject.Tracks[T][C];
- if (Clip.TlEnd > From_) and (Clip.TlStart < To_) then
+ if (Clip.TlEnd > From_ + Eps) and (Clip.TlStart < To_ - Eps) then
Clip.Selected := True;
end;
@@ -3189,6 +3387,7 @@ begin
SelectAll;
Key := 0;
end;
+
VK_HOME:
begin
{ К началу самого раннего клипа, а не просто в ноль: если монтаж
diff --git a/tests/lfmcheck/lfmcheck.lpr b/tests/lfmcheck/lfmcheck.lpr
index aaa3fea..0765eef 100644
--- a/tests/lfmcheck/lfmcheck.lpr
+++ b/tests/lfmcheck/lfmcheck.lpr
@@ -631,6 +631,128 @@ begin
end;
end;
+{ Рез и курсор: где режет S и куда после этого встаёт курсор.
+
+ Раньше курсор оставался там, где был, а рез мог попасть на долю кадра.
+ Теперь рез кладётся на кадровую сетку, а курсор встаёт ровно на место
+ реза — с ним и работают дальше. }
+procedure CheckSplitAndPlayhead;
+var
+ Proj: TProject;
+ Info: TMediaInfo;
+ Media: TMediaItem;
+ Timeline: TTimelineControl;
+ Step: Double;
+ I, N: Integer;
+begin
+ WriteLn;
+ WriteLn('Рез, промежуток и курсор');
+ WriteLn;
+
+ Info := TestInfo;
+ Info.FpsNum := 60000;
+ Info.FpsDen := 1001;
+ Step := Info.FpsDen / Info.FpsNum;
+
+ Proj := TProject.Create;
+ Timeline := TTimelineControl.Create(nil);
+ try
+ Proj.Clear;
+ Proj.AddTrack('V1');
+ Proj.AddTrack('A1');
+ Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
+ Proj.Master := Info;
+ { По два клипа на каждой дорожке, встык: 0..10 и 10..20. }
+ Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
+ Proj.AddClip(Proj.Tracks[0], Media, 20, 10, 10);
+ Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 0);
+ Proj.AddClip(Proj.Tracks[1], Media, 20, 10, 10);
+
+ Timeline.Project := Proj;
+ Timeline.PixelsPerSecond := 100;
+
+ { Рез по курсору: место кладётся на кадр, курсор встаёт на него. }
+ Timeline.Playhead := 3.3177;
+ CheckTrue('разрезаны обе дорожки', Timeline.SplitAtPlayhead(False) = 2);
+ CheckTrue('курсор встал на место реза',
+ Abs(Timeline.Playhead - Round(3.3177 / Step) * Step) < 0.0001);
+ CheckTrue('курсор на кадровой сетке',
+ Abs(Timeline.Playhead / Step - Round(Timeline.Playhead / Step)) < 0.001);
+ N := 0;
+ for I := 0 to Proj.Tracks[0].Count - 1 do
+ if Abs(Proj.Tracks[0][I].TlStart - Timeline.Playhead) < 0.0001 then
+ Inc(N);
+ CheckTrue('рез пришёлся ровно туда, где курсор', N = 1);
+
+ { Промежуток по времени: рез по обоим краям, курсор — на начало. }
+ Timeline.SelectionMode := smRange;
+ Timeline.SetSelectionRange(12.13, 16.77);
+ CheckTrue('промежуток отмечен', Timeline.HasSelectionRange);
+ CheckTrue('разрезано по два клипа на дорожке',
+ Timeline.SplitAtPlayhead(False) = 4);
+ CheckTrue('курсор — на начале промежутка',
+ Abs(Timeline.Playhead - Timeline.RangeFrom) < 0.0001);
+
+ N := 0;
+ for I := 0 to Proj.Tracks[1].Count - 1 do
+ if (Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeFrom) < 0.0001) or
+ (Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeTo) < 0.0001) then
+ Inc(N);
+ CheckTrue('на звуковой дорожке резы по обоим краям', N = 2);
+
+ { После реза выделена середина — то, что между краями: следующим
+ действием её обычно удаляют. }
+ N := 0;
+ for I := 0 to Proj.Tracks[0].Count - 1 do
+ if Proj.Tracks[0][I].Selected then
+ begin
+ Inc(N);
+ CheckTrue('выделенное лежит внутри промежутка',
+ (Proj.Tracks[0][I].TlStart >= Timeline.RangeFrom - 0.0001) and
+ (Proj.Tracks[0][I].TlEnd <= Timeline.RangeTo + 0.0001));
+ end;
+ CheckTrue('на дорожке картинки выделена одна середина', N = 1);
+
+ { Смена режима снимает промежуток: резать по невидимой метке нельзя. }
+ Timeline.SelectionMode := smClips;
+ CheckTrue('смена режима сняла промежуток',
+ not Timeline.HasSelectionRange);
+
+ { Удаление со схлопыванием: курсор встаёт на место стыка. }
+ Proj.Clear;
+ Proj.AddTrack('V1');
+ Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
+ Proj.Master := Info;
+ Proj.RippleEnabled := True;
+ Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
+ Proj.AddClip(Proj.Tracks[0], Media, 10, 5, 5);
+ Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 10);
+ Timeline.Playhead := 0;
+ Proj.Tracks[0][1].Selected := True;
+ Timeline.DeleteSelected;
+ CheckTrue('после удаления клипов стало два', Proj.Tracks[0].Count = 2);
+ CheckTrue('курсор — на месте вырезанного куска',
+ Abs(Timeline.Playhead - 5) < 0.0001);
+ CheckTrue('хвост придвинут к стыку',
+ Abs(Proj.Tracks[0][1].TlStart - 5) < 0.0001);
+
+ { Схлопывание дыры с клавиатуры: курсор тоже на стык. }
+ Proj.Clear;
+ Proj.AddTrack('V1');
+ Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
+ Proj.Master := Info;
+ Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
+ Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
+ Timeline.ActiveTrack := 0;
+ Timeline.Playhead := 8;
+ CheckTrue('дыра схлопнута', Timeline.RippleCloseGap);
+ CheckTrue('курсор — на стыке', Abs(Timeline.Playhead - 5) < 0.0001);
+ finally
+ Timeline.Free;
+ Proj.Free;
+ end;
+end;
+
begin
WriteLn('Свойства из файлов форм');
WriteLn;
@@ -663,6 +785,7 @@ begin
CheckClipCopy;
CheckRubberBand;
CheckFrameGridRepair;
+ CheckSplitAndPlayhead;
CheckSplitAllTracks;
CheckGapTolerance;
diff --git a/tools/make_icons.py b/tools/make_icons.py
index 7ccf0a8..a26d691 100644
--- a/tools/make_icons.py
+++ b/tools/make_icons.py
@@ -31,6 +31,8 @@ ICONS = [
('Log', 'icons8-property-30.png', 24),
('Glue', 'icons8-клей-30.png', 24),
('Target', 'icons8-цель-30.png', 24),
+ ('SelectClips', 'icons8-select-all-90.png', 24),
+ ('SelectRange', 'icons8-width-90.png', 24),
]
# Значок самой программы: отдельно, потому что окну нужен формат ico —