nlve/src/uSettings.pas
4aiman e12d0df974 Напоминание о зависимостях и починка падения под Wine
Падение. Окно зависимостей роняло программу под Wine: «stack overflow»,
шестнадцать мегабайт стека съедались целиком. Причина — качели между
таблицей и своими полосами прокрутки: появление вертикальной полосы
сужает таблицу, от этого перестаёт помещаться содержимое по ширине,
появляется горизонтальная, от неё убавляется высота — и решение о
вертикальной принимается заново. В комментарии стояло обещание, что
второго круга не будет; под Wine круг не сходился никогда.

Теперь глубина пересчёта ограничена тремя кругами. Лишняя полоса
прокрутки не беда, падение — беда.

Искалось на невидимом экране в отдельном префиксе Wine: программа сама
писала след по шагам, и он показал, что окно создаётся целиком, а падает
на показе. Дальше отключением по одному проверены обработчики
перерисовки рамки, подновление полосы меню по таймеру и значок окна —
ни при чём.

Напоминание. Если не хватает ffmpeg или libmpv, при запуске появляется
окно: чего именно нет, и три кнопки — открыть окно зависимостей,
отложить, больше не напоминать. Последнее запоминается в настройках.
Для этого появился ThemedChoiceDlg: диалог со своими кнопками, готовых
«Да / Нет / Отмена» тут не хватало.

Высота надписи в обоих диалогах считалась по общей ширине текста и не
видела переводов строк — сообщение обрезалось снизу. Теперь каждая
строка считается отдельно.
2026-08-22 12:37:35 +03:00

259 lines
8.8 KiB
ObjectPascal
Raw 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.

{
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.