Падение. Окно зависимостей роняло программу под Wine: «stack overflow», шестнадцать мегабайт стека съедались целиком. Причина — качели между таблицей и своими полосами прокрутки: появление вертикальной полосы сужает таблицу, от этого перестаёт помещаться содержимое по ширине, появляется горизонтальная, от неё убавляется высота — и решение о вертикальной принимается заново. В комментарии стояло обещание, что второго круга не будет; под Wine круг не сходился никогда. Теперь глубина пересчёта ограничена тремя кругами. Лишняя полоса прокрутки не беда, падение — беда. Искалось на невидимом экране в отдельном префиксе Wine: программа сама писала след по шагам, и он показал, что окно создаётся целиком, а падает на показе. Дальше отключением по одному проверены обработчики перерисовки рамки, подновление полосы меню по таймеру и значок окна — ни при чём. Напоминание. Если не хватает ffmpeg или libmpv, при запуске появляется окно: чего именно нет, и три кнопки — открыть окно зависимостей, отложить, больше не напоминать. Последнее запоминается в настройках. Для этого появился ThemedChoiceDlg: диалог со своими кнопками, готовых «Да / Нет / Отмена» тут не хватало. Высота надписи в обоих диалогах считалась по общей ширине текста и не видела переводов строк — сообщение обрезалось снизу. Теперь каждая строка считается отдельно.
259 lines
8.8 KiB
ObjectPascal
259 lines
8.8 KiB
ObjectPascal
{
|
||
uSettings — настройки программы между запусками.
|
||
|
||
Хранится то, что раздражает настраивать заново каждый раз: размер окна,
|
||
каталоги, из которых последний раз брали файлы, высота дорожек и
|
||
положение переключателя схлопывания.
|
||
|
||
Формат — JSON, файл лежит в обычном для системы месте для настроек
|
||
(~/.config/nlve на Linux, каталог приложения на Windows). Ни одно
|
||
значение не является обязательным: испорченный или отсутствующий файл
|
||
означает лишь работу со значениями по умолчанию, а не отказ запускаться.
|
||
}
|
||
unit uSettings;
|
||
|
||
{$mode objfpc}{$H+}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils;
|
||
|
||
type
|
||
TSettings = class
|
||
private
|
||
FWindowLeft: Integer;
|
||
FWindowTop: Integer;
|
||
FWindowWidth: Integer;
|
||
FWindowHeight: Integer;
|
||
FWindowMaximized: Boolean;
|
||
FLastMediaDir: string;
|
||
FLastProjectDir: string;
|
||
FLastExportDir: string;
|
||
FRippleAllTracks: Boolean;
|
||
FRippleEnabled: Boolean;
|
||
FTrackHeight: Integer;
|
||
FWaveGain: Double;
|
||
FVideoPanelHeight: Integer;
|
||
FLogVisible: Boolean;
|
||
FDepsReminder: Boolean;
|
||
FRecentProjects: TStringList;
|
||
function FileName: string;
|
||
public
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
procedure Load;
|
||
procedure Save;
|
||
|
||
{ Заносит проект в начало списка недавних. Повтор поднимается наверх,
|
||
а не задваивается. }
|
||
procedure NoteRecentProject(const APath: string);
|
||
property RecentProjects: TStringList read FRecentProjects;
|
||
|
||
property WindowLeft: Integer read FWindowLeft write FWindowLeft;
|
||
property WindowTop: Integer read FWindowTop write FWindowTop;
|
||
property WindowWidth: Integer read FWindowWidth write FWindowWidth;
|
||
property WindowHeight: Integer read FWindowHeight write FWindowHeight;
|
||
property WindowMaximized: Boolean read FWindowMaximized write FWindowMaximized;
|
||
property LastMediaDir: string read FLastMediaDir write FLastMediaDir;
|
||
property LastProjectDir: string read FLastProjectDir write FLastProjectDir;
|
||
property LastExportDir: string read FLastExportDir write FLastExportDir;
|
||
property RippleAllTracks: Boolean read FRippleAllTracks write FRippleAllTracks;
|
||
property RippleEnabled: Boolean read FRippleEnabled write FRippleEnabled;
|
||
property TrackHeight: Integer read FTrackHeight write FTrackHeight;
|
||
{ Во сколько раз растянута форма звука на клипах. Только рисунок. }
|
||
property WaveGain: Double read FWaveGain write FWaveGain;
|
||
property VideoPanelHeight: Integer read FVideoPanelHeight write FVideoPanelHeight;
|
||
{ Видна ли нижняя полоса с журналом. По умолчанию видна: пока
|
||
программа молодая, её сообщения нужнее, чем лишние строки монтажа. }
|
||
property LogVisible: Boolean read FLogVisible write FLogVisible;
|
||
{ Напоминать ли о недостающих зависимостях при запуске. Гасится
|
||
кнопкой «Не напоминать» в самом напоминании. }
|
||
property DepsReminder: Boolean read FDepsReminder write FDepsReminder;
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses
|
||
fpjson, jsonparser, jsonscanner;
|
||
|
||
const
|
||
DefaultWidth = 1600;
|
||
DefaultHeight = 900;
|
||
DefaultTrackHeight = 64;
|
||
DefaultWaveGain = 1.0;
|
||
DefaultVideoHeight = 400;
|
||
|
||
{ Больше десятка в меню не помещается по-человечески, да и не нужно. }
|
||
MaxRecentProjects = 10;
|
||
|
||
constructor TSettings.Create;
|
||
begin
|
||
inherited Create;
|
||
FRecentProjects := TStringList.Create;
|
||
FWindowLeft := -1;
|
||
FWindowTop := -1;
|
||
FWindowWidth := DefaultWidth;
|
||
FWindowHeight := DefaultHeight;
|
||
FTrackHeight := DefaultTrackHeight;
|
||
FWaveGain := DefaultWaveGain;
|
||
FVideoPanelHeight := DefaultVideoHeight;
|
||
FLogVisible := True;
|
||
FDepsReminder := True;
|
||
end;
|
||
|
||
destructor TSettings.Destroy;
|
||
begin
|
||
FRecentProjects.Free;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TSettings.NoteRecentProject(const APath: string);
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
if APath = '' then
|
||
Exit;
|
||
Idx := FRecentProjects.IndexOf(APath);
|
||
if Idx >= 0 then
|
||
FRecentProjects.Delete(Idx);
|
||
FRecentProjects.Insert(0, APath);
|
||
while FRecentProjects.Count > MaxRecentProjects do
|
||
FRecentProjects.Delete(FRecentProjects.Count - 1);
|
||
end;
|
||
|
||
function TSettings.FileName: string;
|
||
begin
|
||
Result := IncludeTrailingPathDelimiter(GetAppConfigDir(False)) + 'settings.json';
|
||
end;
|
||
|
||
procedure TSettings.Load;
|
||
var
|
||
Text: TStringList;
|
||
Parser: TJSONParser;
|
||
Root: TJSONData;
|
||
Obj: TJSONObject;
|
||
Arr: TJSONArray;
|
||
I: Integer;
|
||
begin
|
||
if not FileExists(FileName) then
|
||
Exit;
|
||
|
||
Text := TStringList.Create;
|
||
try
|
||
try
|
||
Text.LoadFromFile(FileName);
|
||
Parser := TJSONParser.Create(Text.Text, [joUTF8]);
|
||
try
|
||
Root := Parser.Parse;
|
||
finally
|
||
Parser.Free;
|
||
end;
|
||
except
|
||
{ Испорченный файл настроек — не повод не запускаться. }
|
||
Exit;
|
||
end;
|
||
finally
|
||
Text.Free;
|
||
end;
|
||
|
||
if (Root = nil) or (not (Root is TJSONObject)) then
|
||
begin
|
||
Root.Free;
|
||
Exit;
|
||
end;
|
||
|
||
try
|
||
Obj := TJSONObject(Root);
|
||
FWindowLeft := Obj.Get('window_left', FWindowLeft);
|
||
FWindowTop := Obj.Get('window_top', FWindowTop);
|
||
FWindowWidth := Obj.Get('window_width', FWindowWidth);
|
||
FWindowHeight := Obj.Get('window_height', FWindowHeight);
|
||
FWindowMaximized := Obj.Get('window_maximized', FWindowMaximized);
|
||
FLastMediaDir := Obj.Get('last_media_dir', FLastMediaDir);
|
||
FLastProjectDir := Obj.Get('last_project_dir', FLastProjectDir);
|
||
FLastExportDir := Obj.Get('last_export_dir', FLastExportDir);
|
||
FRippleAllTracks := Obj.Get('ripple_all_tracks', FRippleAllTracks);
|
||
FRippleEnabled := Obj.Get('ripple_enabled', FRippleEnabled);
|
||
FTrackHeight := Obj.Get('track_height', FTrackHeight);
|
||
FWaveGain := Obj.Get('wave_gain', FWaveGain);
|
||
FVideoPanelHeight := Obj.Get('video_panel_height', FVideoPanelHeight);
|
||
FLogVisible := Obj.Get('log_visible', FLogVisible);
|
||
FDepsReminder := Obj.Get('deps_reminder', FDepsReminder);
|
||
|
||
Arr := Obj.Find('recent_projects', jtArray) as TJSONArray;
|
||
FRecentProjects.Clear;
|
||
if Arr <> nil then
|
||
for I := 0 to Arr.Count - 1 do
|
||
{ Пропавшие файлы в список не тянем: предлагать открыть то, чего
|
||
нет, бессмысленно. }
|
||
if FileExists(Arr.Strings[I]) then
|
||
FRecentProjects.Add(Arr.Strings[I]);
|
||
finally
|
||
Root.Free;
|
||
end;
|
||
|
||
{ Заведомо негодные значения приводим к разумным: файл настроек могли
|
||
поправить руками, а окно высотой в ноль пикселей не откроешь. }
|
||
if FWindowWidth < 640 then
|
||
FWindowWidth := DefaultWidth;
|
||
if FWindowHeight < 480 then
|
||
FWindowHeight := DefaultHeight;
|
||
if (FWaveGain < 1) or (FWaveGain > 64) then
|
||
FWaveGain := DefaultWaveGain;
|
||
if (FTrackHeight < 24) or (FTrackHeight > 300) then
|
||
FTrackHeight := DefaultTrackHeight;
|
||
if FVideoPanelHeight < 100 then
|
||
FVideoPanelHeight := DefaultVideoHeight;
|
||
end;
|
||
|
||
procedure TSettings.Save;
|
||
var
|
||
Root: TJSONObject;
|
||
Text: TStringList;
|
||
Arr: TJSONArray;
|
||
I: Integer;
|
||
begin
|
||
Root := TJSONObject.Create;
|
||
try
|
||
Root.Add('window_left', FWindowLeft);
|
||
Root.Add('window_top', FWindowTop);
|
||
Root.Add('window_width', FWindowWidth);
|
||
Root.Add('window_height', FWindowHeight);
|
||
Root.Add('window_maximized', FWindowMaximized);
|
||
Root.Add('last_media_dir', FLastMediaDir);
|
||
Root.Add('last_project_dir', FLastProjectDir);
|
||
Root.Add('last_export_dir', FLastExportDir);
|
||
Root.Add('ripple_all_tracks', FRippleAllTracks);
|
||
Root.Add('ripple_enabled', FRippleEnabled);
|
||
Root.Add('track_height', FTrackHeight);
|
||
Root.Add('wave_gain', FWaveGain);
|
||
Root.Add('video_panel_height', FVideoPanelHeight);
|
||
Root.Add('log_visible', FLogVisible);
|
||
Root.Add('deps_reminder', FDepsReminder);
|
||
|
||
Arr := TJSONArray.Create;
|
||
for I := 0 to FRecentProjects.Count - 1 do
|
||
Arr.Add(FRecentProjects[I]);
|
||
Root.Add('recent_projects', Arr);
|
||
|
||
if not ForceDirectories(ExtractFilePath(FileName)) then
|
||
Exit;
|
||
|
||
Text := TStringList.Create;
|
||
try
|
||
Text.Text := Root.FormatJSON;
|
||
try
|
||
Text.SaveToFile(FileName);
|
||
except
|
||
{ Не смогли сохранить настройки — работать это не мешает. }
|
||
end;
|
||
finally
|
||
Text.Free;
|
||
end;
|
||
finally
|
||
Root.Free;
|
||
end;
|
||
end;
|
||
|
||
end.
|