diff --git a/AppTheme.pas b/AppTheme.pas new file mode 100644 index 0000000..0e512e4 --- /dev/null +++ b/AppTheme.pas @@ -0,0 +1,303 @@ +unit AppTheme; + +{ Цветовые темы приложения EWSDR. + Определяет TAppTheme — запись со всеми цветами интерфейса. + Два конструктора: DarkTheme() и LightTheme(). } + +{$mode objfpc}{$H+} + +interface + +uses Graphics; + +type + TAppTheme = record + // Форма / панели + BG, Panel, Border: TColor; + // Текст + Text, TextDim: TColor; + // Обычные кнопки (StyleButton) + BtnNorm, BtnActive, BtnHot: TColor; + BtnBorderNorm, BtnBorderActive: TColor; + BtnText, BtnTextActive: TColor; + // Кнопки SPAN / VFO-copy / HidePanel (StyleSpanButton) + SpanNorm, SpanActive: TColor; + SpanBorder: TColor; + SpanText: TColor; + // Кнопки тулбара (Discover / StartStop) + TbDiscoverNorm, TbDiscoverHot, TbDiscoverBorder: TColor; + TbDiscoverText: TColor; + TbStartNorm, TbStartBorder, TbStartText: TColor; + // VFO-select кнопка — активный VFO + VfoSelActNorm, VfoSelActBorder: TColor; + VfoSelActText, VfoSelActTextAct: TColor; + // VFO-select кнопка — неактивный VFO + VfoSelInaNorm, VfoSelInaBorder: TColor; + VfoSelInaText, VfoSelInaTextAct: TColor; + // VFO-TX кнопка — активна (TX на этом VFO) + VfoTxActNorm, VfoTxActBorder: TColor; + VfoTxActText: TColor; + // VFO-TX кнопка — неактивна + VfoTxInaNorm, VfoTxInaBorder: TColor; + VfoTxInaText: TColor; + // Дисплей частоты + FreqActive, FreqDim: TColor; + // Индикаторы (S-meter, мощность) + MeterOn, MeterOvr: TColor; + Amber: TColor; + // Спектр — фон и сетка + SpecGradTop, SpecGradBot: TColor; + SpecLabelBand: TColor; + SpecLabelText: TColor; + SpecGrid: TColor; + // Спектр — сигнал и маркеры + SpecLine: TColor; + SpecFilter: TColor; + SpecFilterEdge: TColor; + SpecAgcColor: TColor; + SpecAgcHangColor: TColor; + SpecVfoCursor: TColor; + // Спектр — градиент заливки (каналы Blue/Green/Red в сыром пикселе) + SpecGradB, SpecGradG, SpecGradR: Byte; // при Alpha=200 + // Линейка частот + RulerGradTop, RulerGradBot: TColor; + RulerBorder: TColor; + RulerText: TColor; + RulerVfo: TColor; + // Ползунки + SliderBG: TColor; + SliderTrackEmpty: TColor; + SliderTrackFill: TColor; + SliderThumbNorm: TColor; + SliderThumbHot: TColor; + SliderThumbDrag: TColor; + SliderThumbBdr: TColor; + // S-метр (широкий) + SMeterBG: TColor; + SMeterBarGreen: TColor; + SMeterBarOver: TColor; + SMeterPeakMarker: TColor; + SMeterTickGreen: TColor; + SMeterTickWhite: TColor; + SMeterTickBlue: TColor; + SMeterLabelDbm: TColor; + SMeterLabelS: TColor; + SMeterScaleDbm: TColor; + SMeterScaleSGreen: TColor; + SMeterScaleSBlue: TColor; + SMeterBdr: TColor; + SMeterDivider: TColor; + end; + +function DarkTheme: TAppTheme; +function LightTheme: TAppTheme; + +implementation + +function DarkTheme: TAppTheme; +begin + FillChar(Result, SizeOf(Result), 0); + Result.BG := TColor($00101010); + Result.Panel := TColor($00181818); + Result.Border := TColor($00303030); + Result.Text := TColor($00CCCCCC); + Result.TextDim := TColor($00666666); + + Result.BtnNorm := TColor($001A1A1A); + Result.BtnActive := TColor($000C3010); + Result.BtnHot := TColor($00282828); + Result.BtnBorderNorm := TColor($00444444); + Result.BtnBorderActive := TColor($00387838); + Result.BtnText := TColor($00CCCCCC); + Result.BtnTextActive := TColor($0000FF88); + + Result.SpanNorm := TColor($00543F18); + Result.SpanActive := TColor($00624B1D); + Result.SpanBorder := TColor($00685624); + Result.SpanText := TColor($00D8E6EE); + + Result.TbDiscoverNorm := TColor($00101828); + Result.TbDiscoverHot := TColor($00182038); + Result.TbDiscoverBorder := TColor($00304860); + Result.TbDiscoverText := TColor($0090B8D0); + Result.TbStartNorm := TColor($00101810); + Result.TbStartBorder := TColor($00304830); + Result.TbStartText := TColor($0090C090); + + Result.VfoSelActNorm := TColor($00101C10); + Result.VfoSelActBorder := TColor($004A7A4A); + Result.VfoSelActText := TColor($0060B860); + Result.VfoSelActTextAct := TColor($0040FF60); + Result.VfoSelInaNorm := TColor($001A1A1A); + Result.VfoSelInaBorder := TColor($00404040); + Result.VfoSelInaText := TColor($00666666); + Result.VfoSelInaTextAct := TColor($00CCCCCC); + + Result.VfoTxActNorm := TColor($00000060); + Result.VfoTxActBorder := TColor($001010B0); + Result.VfoTxActText := TColor($00C8D8FF); + Result.VfoTxInaNorm := TColor($00303030); + Result.VfoTxInaBorder := TColor($00404040); + Result.VfoTxInaText := TColor($00808080); + + Result.FreqActive := TColor($0000FF00); + Result.FreqDim := TColor($00007800); + + Result.MeterOn := TColor($0000CC44); + Result.MeterOvr := TColor($000000CC); + Result.Amber := TColor($0000AAFF); + + Result.SpecGradTop := TColor($0020160B); + Result.SpecGradBot := TColor($006C4F1F); + Result.SpecLabelBand := TColor($00543F18); + Result.SpecLabelText := TColor($00A8C9D9); + Result.SpecGrid := TColor($00685624); + Result.SpecLine := TColor($0040FF80); + Result.SpecFilter := TColor($006A4E22); + Result.SpecFilterEdge := TColor($0040FFCC); + Result.SpecAgcColor := TColor($0000AAFF); + Result.SpecAgcHangColor := TColor($00FFCC00); + Result.SpecVfoCursor := TColor($0000AAFF); + Result.SpecGradB := 100; Result.SpecGradG := 200; Result.SpecGradR := 0; + + Result.RulerGradTop := TColor($00372A12); + Result.RulerGradBot := TColor($00705A2A); + Result.RulerBorder := TColor($00685624); + Result.RulerText := TColor($00C8D8E2); + Result.RulerVfo := TColor($0000FF80); + + Result.SliderBG := TColor($00181818); + Result.SliderTrackEmpty := TColor($00323232); + Result.SliderTrackFill := TColor($00205018); + Result.SliderThumbNorm := TColor($00407840); + Result.SliderThumbHot := TColor($0066BB66); + Result.SliderThumbDrag := TColor($0099EE99); + Result.SliderThumbBdr := TColor($00122012); + + Result.SMeterBG := TColor($00181818); + Result.SMeterBarGreen := TColor($00091F00); + Result.SMeterBarOver := TColor($00091528); + Result.SMeterPeakMarker := TColor($00F8F8FF); + Result.SMeterTickGreen := TColor($00708070); + Result.SMeterTickWhite := TColor($00E0E0DC); + Result.SMeterTickBlue := TColor($0070A090); + Result.SMeterLabelDbm := TColor($0096D8DC); + Result.SMeterLabelS := TColor($00F0F0F0); + Result.SMeterScaleDbm := TColor($00D0DCDC); + Result.SMeterScaleSGreen:= TColor($00E0E0E0); + Result.SMeterScaleSBlue := TColor($0090D090); + Result.SMeterBdr := TColor($00303028); + Result.SMeterDivider := TColor($00404040); +end; + +function LightTheme: TAppTheme; +begin + FillChar(Result, SizeOf(Result), 0); + // Фон — тёплый белый, панели — тёплый светло-серый + Result.BG := TColor($00F0EEE8); + Result.Panel := TColor($00E8E6E0); + Result.Border := TColor($00B8B4AC); + Result.Text := TColor($00181410); + Result.TextDim := TColor($00706860); + + // Обычные кнопки — светло-серые, активная — светло-зелёная + Result.BtnNorm := TColor($00DCDAD4); + Result.BtnActive := TColor($00B8D8B8); + Result.BtnHot := TColor($00CFCDC6); + Result.BtnBorderNorm := TColor($00A8A4A0); + Result.BtnBorderActive := TColor($00407840); + Result.BtnText := TColor($00181410); + Result.BtnTextActive := TColor($00004800); + + // Span/VFO-copy — тёплый тан/пергамент + Result.SpanNorm := TColor($00D4C8A0); + Result.SpanActive := TColor($00C0B488); + Result.SpanBorder := TColor($00A09070); + Result.SpanText := TColor($00302010); + + // Тулбар — более насыщенные оттенки + Result.TbDiscoverNorm := TColor($00C8D8E8); + Result.TbDiscoverHot := TColor($00B8CCE0); + Result.TbDiscoverBorder := TColor($00789CB8); + Result.TbDiscoverText := TColor($00284860); + Result.TbStartNorm := TColor($00C8E0C8); + Result.TbStartBorder := TColor($00609860); + Result.TbStartText := TColor($00204820); + + // VFO-select активный — зелёная подсветка + Result.VfoSelActNorm := TColor($00C0E0C0); + Result.VfoSelActBorder := TColor($00508050); + Result.VfoSelActText := TColor($00205020); + Result.VfoSelActTextAct := TColor($00006600); + // VFO-select неактивный — нейтральный + Result.VfoSelInaNorm := TColor($00DCDAD4); + Result.VfoSelInaBorder := TColor($00A8A4A0); + Result.VfoSelInaText := TColor($00706860); + Result.VfoSelInaTextAct := TColor($00181410); + + // VFO-TX — синеватый акцент + Result.VfoTxActNorm := TColor($00C0CCEC); + Result.VfoTxActBorder := TColor($006070C0); + Result.VfoTxActText := TColor($00102060); + Result.VfoTxInaNorm := TColor($00DCDAD4); + Result.VfoTxInaBorder := TColor($00A8A4A0); + Result.VfoTxInaText := TColor($00706860); + + // Дисплей частоты — насыщенный зелёный + Result.FreqActive := TColor($00005500); + Result.FreqDim := TColor($00447044); + + // Индикаторы + Result.MeterOn := TColor($00006820); + Result.MeterOvr := TColor($00AA1010); + Result.Amber := TColor($00904000); + + // Спектр — светлый пергаментный фон, тёмно-зелёный сигнал + Result.SpecGradTop := TColor($00E8E4D8); + Result.SpecGradBot := TColor($00D0CCBE); + Result.SpecLabelBand := TColor($00D8D4C8); + Result.SpecLabelText := TColor($00404038); + Result.SpecGrid := TColor($00B8B4A4); + Result.SpecLine := TColor($00006400); // тёмно-зелёная линия + Result.SpecFilter := TColor($00C8DCC8); // светло-зелёная полоса фильтра + Result.SpecFilterEdge := TColor($00407840); + Result.SpecAgcColor := TColor($00904000); // тёмно-оранжевый AGC + Result.SpecAgcHangColor := TColor($00802000); + Result.SpecVfoCursor := TColor($00004800); + // Пиксели градиента: тёмно-зелёный (B=0, G=80, R=10 при Alpha=200) + Result.SpecGradB := 0; Result.SpecGradG := 80; Result.SpecGradR := 10; + + // Линейка — тёплый пергамент + Result.RulerGradTop := TColor($00DDD8CC); + Result.RulerGradBot := TColor($00C8C2B4); + Result.RulerBorder := TColor($00A09878); + Result.RulerText := TColor($00302820); + Result.RulerVfo := TColor($00006800); + + // Ползунки — светлые с зелёным акцентом + Result.SliderBG := TColor($00E8E6E0); + Result.SliderTrackEmpty := TColor($00CCCAC4); + Result.SliderTrackFill := TColor($0080AA80); + Result.SliderThumbNorm := TColor($00508050); + Result.SliderThumbHot := TColor($003D9040); + Result.SliderThumbDrag := TColor($0028A830); + Result.SliderThumbBdr := TColor($00183018); + + // S-метр — светлый + Result.SMeterBG := TColor($00ECE8E0); + Result.SMeterBarGreen := TColor($00C0EAC0); + Result.SMeterBarOver := TColor($00EAC0C0); + Result.SMeterPeakMarker := TColor($00181010); + Result.SMeterTickGreen := TColor($00508050); + Result.SMeterTickWhite := TColor($00282018); + Result.SMeterTickBlue := TColor($00406880); + Result.SMeterLabelDbm := TColor($00204860); + Result.SMeterLabelS := TColor($00201810); + Result.SMeterScaleDbm := TColor($00304848); + Result.SMeterScaleSGreen:= TColor($00204020); + Result.SMeterScaleSBlue := TColor($00204050); + Result.SMeterBdr := TColor($00A0A090); + Result.SMeterDivider := TColor($00B0ACA0); +end; + +end. diff --git a/DeviceForm.pas b/DeviceForm.pas index c32cc85..1fbaa33 100644 --- a/DeviceForm.pas +++ b/DeviceForm.pas @@ -5,7 +5,7 @@ unit DeviceForm; interface uses - Classes, SysUtils, FlatButton, Forms, Controls, Graphics, Dialogs, + Classes, SysUtils, FlatButton, AppTheme, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, IniFiles; // Декодирование типа платы (совпадает с MainForm.BoardTypeName) @@ -94,6 +94,8 @@ type public constructor Create(AOwner: TComponent); override; + procedure SetTheme(const T: TAppTheme); + // Добавить найденное устройство (вызывается из MainForm при discovery) procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer = 0); procedure ClearDiscovered; @@ -271,7 +273,69 @@ end; procedure TDeviceDialog.ApplyTheme; begin - // уже задано в BuildUI +end; + +procedure TDeviceDialog.SetTheme(const T: TAppTheme); + + procedure StyleBtn(B: TFlatButton; Accent: Boolean); + begin + B.ClrNorm := T.Panel; + B.ClrBorder := T.Border; + B.ClrHot := T.BG; + B.ClrActive := T.Panel; + B.ClrText := T.Text; + if Accent then B.ClrTextAct := T.FreqActive + else B.ClrTextAct := T.Text; + B.Font.Color := T.Text; + B.Invalidate; + end; + + procedure WalkLabels(C: TWinControl); + var i: Integer; Ctrl: TControl; + begin + for i := 0 to C.ControlCount - 1 do + begin + Ctrl := C.Controls[i]; + if Ctrl is TLabel then + TLabel(Ctrl).Font.Color := T.TextDim + else if Ctrl is TWinControl then + WalkLabels(TWinControl(Ctrl)); + end; + end; + +begin + Color := T.BG; + Font.Color := T.Text; + if PanelLeft <> nil then PanelLeft.Color := T.Panel; + if PanelRight <> nil then PanelRight.Color := T.Panel; + if LstSaved <> nil then begin + LstSaved.Color := T.BG; + LstSaved.Font.Color := T.Text; + end; + if LstFound <> nil then begin + LstFound.Color := T.BG; + LstFound.Font.Color := T.Text; + end; + if BtnAdd <> nil then StyleBtn(BtnAdd, False); + if BtnRemove <> nil then StyleBtn(BtnRemove, False); + if BtnSetAuto <> nil then StyleBtn(BtnSetAuto, False); + if BtnDiscover <> nil then StyleBtn(BtnDiscover, False); + if BtnAddFound <> nil then StyleBtn(BtnAddFound, False); + if BtnCancel <> nil then StyleBtn(BtnCancel, False); + if BtnConnect <> nil then StyleBtn(BtnConnect, True); + // Edit fields: white bg for light theme, dark bg for dark theme + if Integer(T.BG) > Integer(TColor($00808080)) then + begin + if EdName <> nil then begin EdName.Color := TColor($00FFFFFF); EdName.Font.Color := TColor($00202020); end; + if EdIP <> nil then begin EdIP.Color := TColor($00FFFFFF); EdIP.Font.Color := TColor($00202020); end; + end + else + begin + if EdName <> nil then begin EdName.Color := TColor($00121212); EdName.Font.Color := TColor($00E0E0E0); end; + if EdIP <> nil then begin EdIP.Color := TColor($00121212); EdIP.Font.Color := TColor($00E0E0E0); end; + end; + WalkLabels(Self); + Invalidate; end; procedure TDeviceDialog.LoadSaved; diff --git a/FlatSlider.pas b/FlatSlider.pas index 9df066b..28fa5e1 100644 --- a/FlatSlider.pas +++ b/FlatSlider.pas @@ -1,7 +1,8 @@ unit FlatSlider; -{ Кастомный горизонтальный ползунок с тёмной темой — замена TTrackBar. - Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux. } +{ Кастомный горизонтальный ползунок — замена TTrackBar. + Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux. + Цвета настраиваемые: применяй SetThemeColors(T: TAppTheme) для смены темы. } {$mode objfpc}{$H+} @@ -20,6 +21,14 @@ type FDragging: Boolean; FHot: Boolean; FOnChange: TNotifyEvent; + // Цвета (настраиваются через SetThemeColors или напрямую) + FClrBG: TColor; + FClrTrackEmpty: TColor; + FClrTrackFill: TColor; + FClrThumbNorm: TColor; + FClrThumbHot: TColor; + FClrThumbDrag: TColor; + FClrThumbBdr: TColor; procedure SetPosition(V: Integer); procedure SetMin(V: Integer); procedure SetMax(V: Integer); @@ -39,6 +48,11 @@ type public constructor Create(AOwner: TComponent); override; + // Применить набор цветов из темы + procedure SetThemeColors( + ABG, ATrackEmpty, ATrackFill, + AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor); + property Min: Integer read FMin write SetMin; property Max: Integer read FMax write SetMax; property Position: Integer read FPosition write SetPosition; @@ -51,17 +65,9 @@ type implementation const - THUMB_R = 6; // радиус ручки (px) - TRACK_H = 4; // высота рельса (px) - PAD = THUMB_R + 2; // боковой отступ чтобы ручка не вылезала - - CLR_BG = TColor($00181818); // совпадает с CLR_PANEL - CLR_TRACK_EMPTY = TColor($00323232); - CLR_TRACK_FILL = TColor($00205018); // тёмно-зелёный, как тема - CLR_THUMB_NORM = TColor($00407840); - CLR_THUMB_HOT = TColor($0066BB66); - CLR_THUMB_DRAG = TColor($0099EE99); - CLR_THUMB_BDR = TColor($00122012); + THUMB_R = 6; + TRACK_H = 4; + PAD = THUMB_R + 2; constructor TFlatSlider.Create(AOwner: TComponent); begin @@ -73,11 +79,32 @@ begin FDragging := False; FHot := False; Cursor := crHandPoint; + // Тёмная тема по умолчанию + FClrBG := TColor($00181818); + FClrTrackEmpty := TColor($00323232); + FClrTrackFill := TColor($00205018); + FClrThumbNorm := TColor($00407840); + FClrThumbHot := TColor($0066BB66); + FClrThumbDrag := TColor($0099EE99); + FClrThumbBdr := TColor($00122012); +end; + +procedure TFlatSlider.SetThemeColors( + ABG, ATrackEmpty, ATrackFill, + AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor); +begin + FClrBG := ABG; + FClrTrackEmpty := ATrackEmpty; + FClrTrackFill := ATrackFill; + FClrThumbNorm := AThumbNorm; + FClrThumbHot := AThumbHot; + FClrThumbDrag := AThumbDrag; + FClrThumbBdr := AThumbBdr; + Invalidate; end; procedure TFlatSlider.SetPosition(V: Integer); -var - C: Integer; +var C: Integer; begin C := EnsureRange(V, FMin, FMax); if FPosition = C then Exit; @@ -102,10 +129,8 @@ begin Invalidate; end; -// Позиция значения → пиксельная X-координата ручки function TFlatSlider.PosToX(APos: Integer): Integer; -var - Range, TrackW: Integer; +var Range, TrackW: Integer; begin Range := FMax - FMin; TrackW := Width - PAD * 2; @@ -116,10 +141,8 @@ begin Result := PAD + Round((APos - FMin) / Range * TrackW); end; -// Пиксельная X-координата → значение позиции function TFlatSlider.XToPos(AX: Integer): Integer; -var - Range, TrackW, RawX: Integer; +var Range, TrackW, RawX: Integer; begin Range := FMax - FMin; TrackW := Width - PAD * 2; @@ -141,32 +164,29 @@ begin CY := Height div 2; TX := PosToX(FPosition); - Canvas.Brush.Color := CLR_BG; + Canvas.Brush.Color := FClrBG; Canvas.Brush.Style := bsSolid; Canvas.Pen.Style := psClear; Canvas.FillRect(ClientRect); - // Рельс — весь TR := Rect(PAD, CY - TRACK_H div 2, Width - PAD, CY + (TRACK_H + 1) div 2); - Canvas.Brush.Color := CLR_TRACK_EMPTY; + Canvas.Brush.Color := FClrTrackEmpty; Canvas.RoundRect(TR.Left, TR.Top, TR.Right, TR.Bottom, TRACK_H, TRACK_H); - // Рельс — заполненная часть if not FReversed then begin FL := PAD; FR := TX; end else begin FL := TX; FR := Width - PAD; end; if FR > FL then begin - Canvas.Brush.Color := CLR_TRACK_FILL; + Canvas.Brush.Color := FClrTrackFill; Canvas.RoundRect(FL, TR.Top, FR, TR.Bottom, TRACK_H, TRACK_H); end; - // Ручка — круг - if FDragging then ThC := CLR_THUMB_DRAG - else if FHot then ThC := CLR_THUMB_HOT - else ThC := CLR_THUMB_NORM; + if FDragging then ThC := FClrThumbDrag + else if FHot then ThC := FClrThumbHot + else ThC := FClrThumbNorm; Canvas.Brush.Color := ThC; - Canvas.Pen.Color := CLR_THUMB_BDR; + Canvas.Pen.Color := FClrThumbBdr; Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Ellipse(TX - THUMB_R, CY - THUMB_R, diff --git a/MainForm.pas b/MainForm.pas index 391b0ab..3a05929 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -22,7 +22,7 @@ unit MainForm; interface uses - Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, Forms, Controls, Graphics, Dialogs, + Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, AppTheme, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, LCLIntf, LCLType, GraphType, HPSDRProtocol, HPSDRNetwork, @@ -249,6 +249,7 @@ type FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов FDevConnected: Boolean; // True после первого подключения + FLightTheme: Boolean; // True = светлая тема FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO FSpanHz: Double; FSampleRate: Integer; // текущий DDC sample rate (Hz) @@ -345,6 +346,7 @@ type // ---- Helpers ---- procedure BuildUI; procedure ApplyDarkTheme; + procedure SetLightTheme(V: Boolean); procedure StyleButton(B: TFlatButton; Active: Boolean = False); procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False); procedure PositionSpanOverlayButtons; @@ -845,6 +847,7 @@ begin Result.ShowSpectrum := FShowSpectrum; Result.ShowWaterfall := FShowWaterfall; Result.DisplayFPS := FDisplayFPS; + Result.LightTheme := FLightTheme; // Audio device names Result.AudioOutDevice := FAudioOutDevName; Result.AudioInDevice := FAudioInDevName; @@ -1053,6 +1056,7 @@ begin {$ENDIF} end; FSettings.LoadCATSettings(FCATLastGlobal); + FLightTheme := FSettings.LoadTheme; InitCATEngine; CATApplySettings(FCATLastGlobal); FSMeterPeak := -130; @@ -2125,12 +2129,20 @@ end; // =========================================================================== procedure TMainForm.ApplyDarkTheme; +var T: TAppTheme; i: Integer; procedure DP(P: TPanel); + begin P.Color := T.Panel; P.Font.Color := T.Text; end; + procedure DL(L: TLabel; Dim: Boolean = True); + begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end; + procedure DS(S: TFlatSlider); begin - P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT; + S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill, + T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr); end; begin - Color := CLR_BG; Font.Color := CLR_TEXT; + if FLightTheme then T := LightTheme else T := DarkTheme; + + Color := T.BG; Font.Color := T.Text; DP(PanelToolbar); DP(PanelLeft); DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB); DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); @@ -2138,38 +2150,107 @@ begin DP(PanelRX); DP(PanelTX); DP(PanelRight); DP(PanelSpanButtons); + PbSpectrum.Color := T.BG; + PbWaterfall.Color := T.BG; + if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel; + if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel; + PbFwdPower.Color := T.BG; + PbSWR.Color := T.BG; - PbSpectrum.Color := CLR_BG; - PbWaterfall.Color := CLR_BG; - if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL; - if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL; - PbFwdPower.Color := CLR_BG; - PbSWR.Color := CLR_BG; + // Метки + DL(LblAGCTop, False); + DL(LblDrv); DL(LblSpan); + LblFwdPwr.Font.Color := T.MeterOn; + LblSWRVal.Font.Color := T.TextDim; + + // Кнопки тулбара + BtnDiscover.ClrNorm := T.TbDiscoverNorm; + BtnDiscover.ClrHot := T.TbDiscoverHot; + BtnDiscover.ClrActive := T.TbDiscoverHot; + BtnDiscover.ClrBorder := T.TbDiscoverBorder; + BtnDiscover.ClrText := T.TbDiscoverText; + BtnDiscover.ClrTextAct := T.TbDiscoverText; + BtnDiscover.Invalidate; + BtnStartStop.ClrNorm := T.TbStartNorm; + BtnStartStop.ClrBorder := T.TbStartBorder; + BtnStartStop.ClrText := T.TbStartText; + BtnStartStop.Invalidate; + + // Обычные кнопки — все массивы + for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active); + for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active); + for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active); + for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active); + StyleButton(BtnCTun, BtnCTun.Active); + StyleButton(BtnNR, BtnNR.Active); + StyleButton(BtnNB, BtnNB.Active); + StyleButton(BtnSNB, BtnSNB.Active); + StyleButton(BtnANF, BtnANF.Active); + StyleButton(BtnMute, BtnMute.Active); + StyleButton(BtnWfAGC, BtnWfAGC.Active); + StyleButton(BtnWfNF, BtnWfNF.Active); + StyleButton(BtnMOX, BtnMOX.Active); + + // Span / VFO-copy / HidePanel + StyleSpanButton(BtnSpan48k, BtnSpan48k.Active); + StyleSpanButton(BtnSpan96k, BtnSpan96k.Active); + StyleSpanButton(BtnSpan192k, BtnSpan192k.Active); + StyleSpanButton(BtnSpan384k, BtnSpan384k.Active); + StyleSpanButton(BtnSpan768k, BtnSpan768k.Active); + StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active); + StyleSpanButton(BtnVfoACopyB, False); + StyleSpanButton(BtnVfoSwap, False); + StyleSpanButton(BtnVfoBCopyA, False); + if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False); + + // Ползунки + DS(TrkAGC); DS(TrkVolume); DS(TrkDrive); + + // Спектр + FSpecView.SetTheme(T); + + // VFO display — вызывает UpdateVfoDisplay, который использует тему + UpdateVfoDisplay; +end; + +procedure TMainForm.SetLightTheme(V: Boolean); +begin + if FLightTheme = V then Exit; + FLightTheme := V; + ApplyDarkTheme; + FSettings.SaveTheme(V); + FSettings.Save; + if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then + TSettingsForm(FSettingsForm).LoadLightTheme(V); end; procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean); +var T: TAppTheme; begin + if FLightTheme then T := LightTheme else T := DarkTheme; B.Active := Active; - B.ClrNorm := CLR_INACTIVE; - B.ClrActive := CLR_ACTIVE; - B.ClrHot := TColor($00282828); - if Active then B.ClrBorder := CLR_ACTIVE_BDR - else B.ClrBorder := TColor($00444444); - B.ClrText := CLR_TEXT; - B.ClrTextAct := TColor($0000FF88); + B.ClrNorm := T.BtnNorm; + B.ClrActive := T.BtnActive; + B.ClrHot := T.BtnHot; + if Active then B.ClrBorder := T.BtnBorderActive + else B.ClrBorder := T.BtnBorderNorm; + B.ClrText := T.BtnText; + B.ClrTextAct := T.BtnTextActive; B.Font.Name := 'Courier New'; B.Font.Size := 8; end; procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean); +var T: TAppTheme; begin + if FLightTheme then T := LightTheme else T := DarkTheme; B.Active := Active; - B.ClrNorm := TColor($00543F18); - B.ClrActive := TColor($00624B1D); - B.ClrHot := TColor($00624B1D); - B.ClrBorder := TColor($00685624); - B.ClrText := TColor($00D8E6EE); - B.ClrTextAct := TColor($00F0FAFF); + B.ClrNorm := T.SpanNorm; + B.ClrActive := T.SpanActive; + B.ClrHot := T.SpanActive; + B.ClrBorder := T.SpanBorder; + B.ClrText := T.SpanText; + B.ClrTextAct := T.SpanText; B.Font.Name := 'Courier New'; B.Font.Size := 7; end; @@ -2246,7 +2327,10 @@ end; procedure TMainForm.UpdateVfoDisplay; const TOP_VFO_FREQ_H = 47; +var T: TAppTheme; begin + if FLightTheme then T := LightTheme else T := DarkTheme; + FreqDispA.Frequency := Round(FVfoA); FreqDispB.Frequency := Round(FVfoB); FreqDispA.FontSize := TOP_VFO_FONT; @@ -2256,56 +2340,52 @@ begin if FActiveVfo = 0 then begin - FreqDispA.ColorNormal := CLR_FREQ; - FreqDispB.ColorNormal := CLR_FREQ_DIM; - LblVfoALabel.Font.Color := CLR_FREQ; - LblVfoBLabel.Font.Color := CLR_TEXTDIM; - // Активный VFO-A: зелёный акцент + FreqDispA.ColorNormal := T.FreqActive; + FreqDispB.ColorNormal := T.FreqDim; + LblVfoALabel.Font.Color := T.FreqActive; + LblVfoBLabel.Font.Color := T.TextDim; if BtnTopVfoASelect <> nil then begin - BtnTopVfoASelect.ClrNorm := TColor($00101C10); - BtnTopVfoASelect.ClrActive := TColor($00182818); - BtnTopVfoASelect.ClrHot := TColor($00182818); - BtnTopVfoASelect.ClrBorder := TColor($004A7A4A); - BtnTopVfoASelect.ClrText := TColor($0060B860); - BtnTopVfoASelect.ClrTextAct := TColor($0040FF60); + BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm; + BtnTopVfoASelect.ClrActive := T.VfoSelActNorm; + BtnTopVfoASelect.ClrHot := T.VfoSelActNorm; + BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder; + BtnTopVfoASelect.ClrText := T.VfoSelActText; + BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct; BtnTopVfoASelect.Active := True; BtnTopVfoASelect.Invalidate; end; - // Неактивный VFO-B: приглушённый if BtnTopVfoBSelect <> nil then begin - BtnTopVfoBSelect.ClrNorm := TColor($001A1A1A); - BtnTopVfoBSelect.ClrActive := TColor($00282828); - BtnTopVfoBSelect.ClrHot := TColor($00252525); - BtnTopVfoBSelect.ClrBorder := TColor($00404040); - BtnTopVfoBSelect.ClrText := CLR_TEXTDIM; - BtnTopVfoBSelect.ClrTextAct := CLR_TEXT; + BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm; + BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm; + BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm; + BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder; + BtnTopVfoBSelect.ClrText := T.VfoSelInaText; + BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct; BtnTopVfoBSelect.Active := False; BtnTopVfoBSelect.Invalidate; end; end else begin - FreqDispA.ColorNormal := CLR_FREQ_DIM; - FreqDispB.ColorNormal := CLR_FREQ; - LblVfoALabel.Font.Color := CLR_TEXTDIM; - LblVfoBLabel.Font.Color := CLR_FREQ; - // Неактивный VFO-A: приглушённый + FreqDispA.ColorNormal := T.FreqDim; + FreqDispB.ColorNormal := T.FreqActive; + LblVfoALabel.Font.Color := T.TextDim; + LblVfoBLabel.Font.Color := T.FreqActive; if BtnTopVfoASelect <> nil then begin - BtnTopVfoASelect.ClrNorm := TColor($001A1A1A); - BtnTopVfoASelect.ClrActive := TColor($00282828); - BtnTopVfoASelect.ClrHot := TColor($00252525); - BtnTopVfoASelect.ClrBorder := TColor($00404040); - BtnTopVfoASelect.ClrText := CLR_TEXTDIM; - BtnTopVfoASelect.ClrTextAct := CLR_TEXT; + BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm; + BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm; + BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm; + BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder; + BtnTopVfoASelect.ClrText := T.VfoSelInaText; + BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct; BtnTopVfoASelect.Active := False; BtnTopVfoASelect.Invalidate; end; - // Активный VFO-B: зелёный акцент if BtnTopVfoBSelect <> nil then begin - BtnTopVfoBSelect.ClrNorm := TColor($00101C10); - BtnTopVfoBSelect.ClrActive := TColor($00182818); - BtnTopVfoBSelect.ClrHot := TColor($00182818); - BtnTopVfoBSelect.ClrBorder := TColor($004A7A4A); - BtnTopVfoBSelect.ClrText := TColor($0060B860); - BtnTopVfoBSelect.ClrTextAct := TColor($0040FF60); + BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm; + BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm; + BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm; + BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder; + BtnTopVfoBSelect.ClrText := T.VfoSelActText; + BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct; BtnTopVfoBSelect.Active := True; BtnTopVfoBSelect.Invalidate; end; @@ -2314,22 +2394,22 @@ begin begin if not FSplitTxB then begin - BtnTopVfoATX.ClrNorm := TColor($00000060); - BtnTopVfoATX.ClrHot := TColor($00000090); - BtnTopVfoATX.ClrActive := TColor($00000090); - BtnTopVfoATX.ClrBorder := TColor($001010B0); - BtnTopVfoATX.ClrText := TColor($00C8D8FF); - BtnTopVfoATX.ClrTextAct := TColor($00E8F0FF); + BtnTopVfoATX.ClrNorm := T.VfoTxActNorm; + BtnTopVfoATX.ClrHot := T.VfoTxActNorm; + BtnTopVfoATX.ClrActive := T.VfoTxActNorm; + BtnTopVfoATX.ClrBorder := T.VfoTxActBorder; + BtnTopVfoATX.ClrText := T.VfoTxActText; + BtnTopVfoATX.ClrTextAct := T.VfoTxActText; BtnTopVfoATX.Active := True; end else begin - BtnTopVfoATX.ClrNorm := TColor($00303030); - BtnTopVfoATX.ClrHot := TColor($00404040); - BtnTopVfoATX.ClrActive := TColor($00404040); - BtnTopVfoATX.ClrBorder := TColor($00505050); - BtnTopVfoATX.ClrText := CLR_TEXTDIM; - BtnTopVfoATX.ClrTextAct := CLR_TEXT; + BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm; + BtnTopVfoATX.ClrHot := T.VfoTxInaNorm; + BtnTopVfoATX.ClrActive := T.VfoTxInaNorm; + BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder; + BtnTopVfoATX.ClrText := T.VfoTxInaText; + BtnTopVfoATX.ClrTextAct := T.VfoTxInaText; BtnTopVfoATX.Active := False; end; BtnTopVfoATX.Invalidate; @@ -2338,22 +2418,22 @@ begin begin if FSplitTxB then begin - BtnTopVfoBTX.ClrNorm := TColor($00000060); - BtnTopVfoBTX.ClrHot := TColor($00000090); - BtnTopVfoBTX.ClrActive := TColor($00000090); - BtnTopVfoBTX.ClrBorder := TColor($001010B0); - BtnTopVfoBTX.ClrText := TColor($00C8D8FF); - BtnTopVfoBTX.ClrTextAct := TColor($00E8F0FF); + BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm; + BtnTopVfoBTX.ClrHot := T.VfoTxActNorm; + BtnTopVfoBTX.ClrActive := T.VfoTxActNorm; + BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder; + BtnTopVfoBTX.ClrText := T.VfoTxActText; + BtnTopVfoBTX.ClrTextAct := T.VfoTxActText; BtnTopVfoBTX.Active := True; end else begin - BtnTopVfoBTX.ClrNorm := TColor($00303030); - BtnTopVfoBTX.ClrHot := TColor($00404040); - BtnTopVfoBTX.ClrActive := TColor($00404040); - BtnTopVfoBTX.ClrBorder := TColor($00505050); - BtnTopVfoBTX.ClrText := CLR_TEXTDIM; - BtnTopVfoBTX.ClrTextAct := CLR_TEXT; + BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm; + BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm; + BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm; + BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder; + BtnTopVfoBTX.ClrText := T.VfoTxInaText; + BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText; BtnTopVfoBTX.Active := False; end; BtnTopVfoBTX.Invalidate; @@ -2907,6 +2987,8 @@ begin if not Assigned(FDeviceDialog) then FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; + if FLightTheme then FDeviceDialog.SetTheme(LightTheme) + else FDeviceDialog.SetTheme(DarkTheme); FDeviceDialog.ShowModal; // Результат диалога не обрабатываем здесь — START сам спросит если нужно end; @@ -2986,6 +3068,8 @@ begin if not Assigned(FDeviceDialog) then FDeviceDialog := TDeviceDialog.Create(Self); FDeviceDialog.OnDiscover := BtnDiscoverFromDialog; + if FLightTheme then FDeviceDialog.SetTheme(LightTheme) + else FDeviceDialog.SetTheme(DarkTheme); if FDeviceDialog.ShowModal <> mrOk then Exit; if not FDeviceDialog.DialogResult.Accepted then Exit; FPendingIP := FDeviceDialog.DialogResult.IPAddress; @@ -5016,6 +5100,7 @@ begin SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; SF.OnCATChange := OnCATSettingsChange; + SF.OnThemeChange := SetLightTheme; end; SF := TSettingsForm(FSettingsForm); SF.LoadPASettings(FPAMaxPower, FPABandCal); @@ -5032,6 +5117,7 @@ begin SF.RefreshAudioDevices(FAudioOut, FAudioIn); SF.LoadVisibility(FShowSpectrum, FShowWaterfall); SF.LoadFPS(FDisplayFPS); + SF.LoadLightTheme(FLightTheme); // Загружаем текущие значения if FWDSPReady then SF.LoadValues( diff --git a/Settings.pas b/Settings.pas index 9d31672..8868d55 100644 --- a/Settings.pas +++ b/Settings.pas @@ -72,6 +72,8 @@ type ShowWaterfall: Boolean; // True = draw waterfall on main form // --- Display FPS --- DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100) + // --- Theme --- + LightTheme: Boolean; // False = dark (default), True = light // --- PA (Power Amplifier) settings --- PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 @@ -128,6 +130,9 @@ type // CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения) procedure SaveCATSettings(const G: TGlobalSettings); procedure LoadCATSettings(var G: TGlobalSettings); + // Тема — глобальная настройка, не привязана к устройству + procedure SaveTheme(ALightTheme: Boolean); + function LoadTheme: Boolean; end; implementation @@ -413,6 +418,7 @@ begin G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True); G.DisplayFPS := JI(GObj,'display_fps',60); + G.LightTheme := JB(GObj,'light_theme',False); // PA settings G.PAMaxPower := JD(GObj,'pa_max_power',100.0); for i := 0 to CFG_BAND_COUNT-1 do @@ -489,6 +495,7 @@ begin JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_waterfall',G.ShowWaterfall); JW(O,'display_fps',G.DisplayFPS); + JW(O,'light_theme',G.LightTheme); // PA settings JW(O,'pa_max_power',G.PAMaxPower); for i := 0 to CFG_BAND_COUNT-1 do @@ -621,4 +628,20 @@ begin end; end; +procedure TSettingsManager.SaveTheme(ALightTheme: Boolean); +var O: TJSONObject; +begin + O := EnsureObj(FRoot, 'preferences'); + JW(O, 'light_theme', ALightTheme); +end; + +function TSettingsManager.LoadTheme: Boolean; +var O: TJSONObject; +begin + Result := False; + if FRoot.Find('preferences') = nil then Exit; + O := EnsureObj(FRoot, 'preferences'); + Result := JB(O, 'light_theme', False); +end; + end. diff --git a/SettingsForm.pas b/SettingsForm.pas index 62a39c9..05ade3c 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -27,7 +27,7 @@ uses Classes, SysUtils, Math, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, Spin, - FlatButton, AudioOutput, AudioInput; + FlatButton, AudioOutput, AudioInput, AppTheme; type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон @@ -43,6 +43,7 @@ type TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object; TOnFPSChange = procedure(FPS: Integer) of object; + TOnThemeChange = procedure(LightTheme: Boolean) of object; TOnCATChange = procedure( const SerEnabled: array of Boolean; const SerPort: array of string; @@ -101,6 +102,7 @@ type FChkShowSpectrum: TCheckBox; FChkShowWaterfall: TCheckBox; FCmbFPS: TComboBox; + FChkLightTheme: TCheckBox; // ---- PA Settings tab controls ---- FEdMaxPower: TSpinEdit; @@ -129,6 +131,7 @@ type FOnAudioInDevChange: TOnAudioInDevChange; FOnVisibilityChange: TOnVisibilityChange; FOnFPSChange: TOnFPSChange; + FOnThemeChange: TOnThemeChange; FLoading: Boolean; @@ -138,7 +141,7 @@ type procedure BuildRX1Tab; procedure BuildPATab; procedure BuildCATTab; - procedure ApplyTheme; + procedure ApplyTheme(const T: TAppTheme); procedure NavClick(Sender: TObject); procedure SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -167,6 +170,7 @@ type procedure OnTXDevChange(Sender: TObject); procedure OnVisibilityChkChange(Sender: TObject); procedure OnFPSCmbChange(Sender: TObject); + procedure OnLightThemeChkChange(Sender: TObject); procedure OnMaxPowerChange(Sender: TObject); procedure OnBandCalChange(Sender: TObject); procedure OnCATSerialChange(Sender: TObject); @@ -203,6 +207,7 @@ type procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); procedure LoadFPS(FPS: Integer); + procedure LoadLightTheme(ALight: Boolean); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); procedure LoadCATSettings( const SerEnabled: array of Boolean; @@ -219,6 +224,7 @@ type property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange; property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange; property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange; + property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange; end; implementation @@ -287,7 +293,7 @@ begin FLoading := False; FRXDevCount := 0; BuildUI; - ApplyTheme; + ApplyTheme(DarkTheme); end; // --------------------------------------------------------------------------- @@ -611,6 +617,18 @@ begin for i := 0 to High(FPS_NAMES) do FCmbFPS.Items.Add(FPS_NAMES[i]); FCmbFPS.ItemIndex := 8; + + Grp := MakeGroupPanel(FPageDisplay, 'Appearance', MARGIN, 292, 650, 68); + + FChkLightTheme := TCheckBox.Create(Self); + FChkLightTheme.Parent := Grp; + FChkLightTheme.Caption := 'Light theme'; + FChkLightTheme.SetBounds(PAD, R1, CHKW, 22); + FChkLightTheme.Checked := False; + FChkLightTheme.Font.Color := CLR_TEXT; + FChkLightTheme.Font.Name := UI_FONT; + FChkLightTheme.Font.Size := 9; + FChkLightTheme.OnChange := OnLightThemeChkChange; end; // --------------------------------------------------------------------------- @@ -888,18 +906,66 @@ begin end; end; -procedure TSettingsForm.ApplyTheme; +procedure TSettingsForm.ApplyTheme(const T: TAppTheme); + + procedure StyleBtn(B: TFlatButton); + begin + B.ClrNorm := T.Panel; + B.ClrBorder := T.Panel; + B.ClrHot := T.BG; + B.ClrActive := T.Panel; + B.ClrText := T.Text; + B.ClrTextAct := T.FreqActive; + B.Invalidate; + end; + + procedure WalkControls(C: TWinControl); + var i: Integer; Ctrl: TControl; + begin + for i := 0 to C.ControlCount - 1 do + begin + Ctrl := C.Controls[i]; + if Ctrl is TFlatButton then + StyleBtn(TFlatButton(Ctrl)) + else if Ctrl is TScrollBox then + begin + TScrollBox(Ctrl).Color := T.BG; + WalkControls(TScrollBox(Ctrl)); + end + else if Ctrl is TPanel then + begin + if TPanel(Ctrl).Height = 1 then + TPanel(Ctrl).Color := T.Border + else + TPanel(Ctrl).Color := T.Panel; + WalkControls(TPanel(Ctrl)); + end + else if Ctrl is TLabel then + begin + if Ctrl.Font.Size >= 12 then + TLabel(Ctrl).Font.Color := T.Text + else if fsBold in Ctrl.Font.Style then + TLabel(Ctrl).Font.Color := T.FreqActive + else + TLabel(Ctrl).Font.Color := T.TextDim; + end + else if Ctrl is TCheckBox then + TCheckBox(Ctrl).Font.Color := T.Text + else if Ctrl is TListBox then + begin + TListBox(Ctrl).Color := T.BG; + TListBox(Ctrl).Font.Color := T.Text; + end + else if Ctrl is TWinControl then + WalkControls(TWinControl(Ctrl)); + end; + end; + begin - Color := CLR_BG; - if Assigned(FNavPanel) then FNavPanel.Color := TColor($00151515); - if Assigned(FContentPanel) then FContentPanel.Color := CLR_BG; - if Assigned(FPageAudio) then FPageAudio.Color := CLR_BG; - if Assigned(FPageDisplay) then FPageDisplay.Color := CLR_BG; - if Assigned(FPageSpectrum) then FPageSpectrum.Color := CLR_BG; - if Assigned(FPageWaterfall) then FPageWaterfall.Color := CLR_BG; - if Assigned(FPagePA) then FPagePA.Color := CLR_BG; - if Assigned(FPageAdvanced) then FPageAdvanced.Color := CLR_BG; - if Assigned(FPageCAT) then FPageCAT.Color := CLR_BG; + Color := T.BG; + Font.Color := T.Text; + WalkControls(Self); + Invalidate; end; // --------------------------------------------------------------------------- @@ -1265,6 +1331,15 @@ begin FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked); end; +procedure TSettingsForm.OnLightThemeChkChange(Sender: TObject); +begin + if FLoading then Exit; + if FChkLightTheme.Checked then ApplyTheme(LightTheme) + else ApplyTheme(DarkTheme); + if Assigned(FOnThemeChange) then + FOnThemeChange(FChkLightTheme.Checked); +end; + procedure TSettingsForm.OnFPSCmbChange(Sender: TObject); var Idx: Integer; @@ -1291,6 +1366,18 @@ begin end; end; +procedure TSettingsForm.LoadLightTheme(ALight: Boolean); +begin + FLoading := True; + try + FChkLightTheme.Checked := ALight; + finally + FLoading := False; + end; + if ALight then ApplyTheme(LightTheme) + else ApplyTheme(DarkTheme); +end; + procedure TSettingsForm.LoadPASettings(MaxPower: Double; const BandCal: array of Double); var i, n: Integer; diff --git a/SpectrumView.pas b/SpectrumView.pas index 7b5d468..826c33d 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -18,7 +18,7 @@ interface uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, - IntfGraphics, FPImage, LCLIntf, LCLType, GraphType; + IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme; const SV_CLR_BG = TColor($00101010); @@ -44,6 +44,9 @@ type FWfIntfImg: TLazIntfImage; FWfPixels: array of LongWord; FSpPts: array of TPoint; + // ── Тема ───────────────────────────────────────────────────────────────── + FTheme: TAppTheme; + FLightTheme: Boolean; FSpPtsLen: Integer; FSmBitmap: TBitmap; FRulerLastFreq: Double; @@ -197,6 +200,8 @@ type // ── Утилиты ─────────────────────────────────────────────────────────────── procedure InvalidateGridCache; + // ── Тема ───────────────────────────────────────────────────────────────── + procedure SetTheme(const T: TAppTheme); procedure InvalidateRulerCache; procedure ResetSpectrumBuf; procedure ResetWfAvgBuf; @@ -360,6 +365,8 @@ begin FWaterfallDirty := True; FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130; FLastFwdW := 0; FLastSWR := 1; + FLightTheme := False; + FTheme := DarkTheme; ResetSpectrumBuf; end; @@ -439,14 +446,17 @@ begin Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200); if Alpha <= 0 then Continue; if Alpha > 200 then Alpha := 200; - GG := Byte(Alpha shr 1); RG := Byte(Alpha); + GG := Byte(FTheme.SpecGradB * Alpha div 200); + RG := Byte(FTheme.SpecGradG * Alpha div 200); RowPtr := FSpecGradImg.GetDataLineStart(Row); if RowPtr = nil then Continue; for Col := 0 to W - 1 do begin if SpPts[Col].Y <= Row then begin - RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0; + RowPtr[0] := GG; + RowPtr[1] := RG; + RowPtr[2] := Byte(FTheme.SpecGradR * Alpha div 200); if BPPg >= 4 then RowPtr[3] := Byte(Alpha); end else begin RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0; @@ -495,7 +505,7 @@ procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer); begin if (W <= 0) or (H <= 0) then Exit; FSpectrumBitmap.SetSize(W, H); - FSpectrumBitmap.Canvas.Brush.Color := SV_CLR_BG; + FSpectrumBitmap.Canvas.Brush.Color := FTheme.BG; FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H)); FreeAndNil(FSpecGradImg); InvalidateGridCache; @@ -532,6 +542,16 @@ begin FSpectrumDirty := True; end; +procedure TSpectrumView.SetTheme(const T: TAppTheme); +begin + FTheme := T; + FLightTheme := T.BG > TColor($00808080); // светлая тема если фон светлее 50% + FreeAndNil(FSpecGradImg); // пересчитать градиент + InvalidateGridCache; + InvalidateRulerCache; + FWaterfallDirty := True; +end; + procedure TSpectrumView.InvalidateRulerCache; begin FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0; @@ -606,20 +626,20 @@ begin FGridBitmapW := W; FGridBitmapH := H; FGridBitmap.SetSize(W, H); GC := FGridBitmap.Canvas; - PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F)); + PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot); LabelBandW := 28; GC.Pen.Style := psClear; GC.Brush.Style := bsSolid; - GC.Brush.Color := TColor($00543F18); + GC.Brush.Color := FTheme.SpecLabelBand; GC.FillRect(Rect(0, 0, LabelBandW, H)); GC.Pen.Style := psSolid; GC.Font.Size := 7; GC.Font.Name := 'Courier New'; - GC.Font.Color := TColor($00A8C9D9); + GC.Font.Color := FTheme.SpecLabelText; GC.Brush.Style := bsClear; dB := DBmax - FSpecGridStep; while dB >= DBmin do begin Yp := Round((DBmax - dB) * InvRange * H); - GC.Pen.Color := TColor($00685624); GC.Pen.Width := 1; + GC.Pen.Color := FTheme.SpecGrid; GC.Pen.Width := 1; GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp); GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); dB := dB - FSpecGridStep; @@ -627,7 +647,7 @@ begin for i := 0 to 8 do begin GX := ScaleX(i, 8, W); - GC.Pen.Color := TColor($00685624); + GC.Pen.Color := FTheme.SpecGrid; GC.MoveTo(GX, 0); GC.LineTo(GX, H); end; end; @@ -646,7 +666,7 @@ begin end; X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W))); X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W))); - C.Brush.Color := TColor($006A4E22); C.Brush.Style := bsSolid; C.Pen.Style := psClear; + C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear; if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); C.Pen.Style := psSolid; @@ -656,26 +676,26 @@ begin AGCy := Round((DBmax - FAGCThresh) * InvRange * H); if (AGCy >= 0) and (AGCy < H) then begin - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; + C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; C.TextOut(4, AGCy - 9, 'AGC T'); end; AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H); if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then begin - C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot; + C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot; C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); - C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6; + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6; C.TextOut(4, AGCHangY + 2, 'AGC H'); end; end else begin AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H); if (AGCy >= 0) and (AGCy < H) then begin - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; + C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); end; end; @@ -700,16 +720,16 @@ begin DrawSpectrumGradient(FSpPts, W, H); // ── 6. Линия спектра ────────────────────────────────────────────────────── - C.Pen.Style := psSolid; C.Pen.Color := TColor($0040FF80); C.Pen.Width := 1; + C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1; C.Polyline(Slice(FSpPts, W)); // ── 7. Края фильтра + VFO ───────────────────────────────────────────────── - C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid; + C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid; C.MoveTo(X1, 0); C.LineTo(X1, H); C.MoveTo(X2, 0); C.LineTo(X2, H); - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2; + C.Pen.Color := FTheme.SpecVfoCursor; C.Pen.Width := 2; C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12); - C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1; + C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1; C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]); if FMarkerActive then DrawMarkerLine(C, W, H); @@ -862,8 +882,8 @@ begin if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then FRulerBitmap.SetSize(W, H); C := FRulerBitmap.Canvas; - PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A)); - C.Pen.Color := TColor($00685624); C.Pen.Width := 1; + PaintVerticalGradient(C, W, H, FTheme.RulerGradTop, FTheme.RulerGradBot); + C.Pen.Color := FTheme.RulerBorder; C.Pen.Width := 1; C.MoveTo(0, 0); C.LineTo(W, 0); C.MoveTo(0, H-1); C.LineTo(W, H-1); C.Font.Name := 'Courier New'; C.Font.Size := 7; @@ -872,18 +892,18 @@ begin for i := 0 to 8 do begin X := ScaleX(i, 8, W); - C.Pen.Color := TColor($00685624); + C.Pen.Color := FTheme.RulerBorder; C.MoveTo(X, 0); C.LineTo(X, H div 2); FreqHz := FreqStart + i * FSpanHz / 8; Lbl := Format('%.3f', [FreqHz / 1e6]); TW := C.TextWidth(Lbl); - C.Font.Color := TColor($00C8D8E2); + C.Font.Color := FTheme.RulerText; C.TextOut(X - TW div 2, H div 2 - 1, Lbl); end; VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W); if (VfoX >= 0) and (VfoX < W) then begin - C.Pen.Color := TColor($0000FF80); C.Pen.Width := 2; + C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2; C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1; end; end; @@ -896,7 +916,7 @@ procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect; Value, MaxVal: Double; BarColor: TColor); var Pct, BarW: Integer; begin - ACanvas.Brush.Color := TColor($00080808); + ACanvas.Brush.Color := FTheme.SMeterBG; ACanvas.FillRect(R); if MaxVal > 0 then Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2)) @@ -918,22 +938,13 @@ const DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20'); S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13); S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60'); - CLR_SMETER_BG = TColor($00181818); - CLR_BAR_GREEN = TColor($00091F00); - CLR_BAR_OVER = TColor($00091528); - CLR_PEAK_MARKER = TColor($00F8F8FF); - CLR_TICK_GREEN = TColor($00708070); - CLR_TICK_WHITE = TColor($00E0E0DC); - CLR_TICK_BLUE = TColor($0070A090); - CLR_LABEL_DBM = TColor($0096D8DC); - CLR_LABEL_S = TColor($00F0F0F0); - CLR_SCALE_DBM = TColor($00D0DCDC); - CLR_SCALE_S_GREEN = TColor($00E0E0E0); - CLR_SCALE_S_BLUE = TColor($0090D090); - CLR_BDR = TColor($00303028); LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4; TICK_LONG = 5; TICK_MED = 3; var + CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor; + CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor; + CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor; + CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor; W, H, BX, BW, LeftInfo: Integer; Y_BAR_TOP, Y_BAR_BOT: Integer; BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer; @@ -960,6 +971,20 @@ var end; begin + CLR_SMETER_BG := FTheme.SMeterBG; + CLR_BAR_GREEN := FTheme.SMeterBarGreen; + CLR_BAR_OVER := FTheme.SMeterBarOver; + CLR_PEAK_MARKER := FTheme.SMeterPeakMarker; + CLR_TICK_GREEN := FTheme.SMeterTickGreen; + CLR_TICK_WHITE := FTheme.SMeterTickWhite; + CLR_TICK_BLUE := FTheme.SMeterTickBlue; + CLR_LABEL_DBM := FTheme.SMeterLabelDbm; + CLR_LABEL_S := FTheme.SMeterLabelS; + CLR_SCALE_DBM := FTheme.SMeterScaleDbm; + CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen; + CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue; + CLR_BDR := FTheme.SMeterBdr; + W := R.Right - R.Left; H := R.Bottom - R.Top; ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0; if (W < 80) or (H < 16) then Exit; @@ -997,9 +1022,9 @@ begin ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid; ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid; - FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006)); - FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006)); - FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10)); + FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, CLR_BAR_GREEN); + FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, CLR_BAR_GREEN); + FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER); BarEnd := DBtoX(Value); PeakLeft := DBtoX(MinVal); @@ -1029,7 +1054,7 @@ begin ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT); // разделитель между зоной надписей и шкалой - ACanvas.Pen.Color := TColor($00404040); + ACanvas.Pen.Color := FTheme.SMeterDivider; ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1); // --- Шкальные тики и надписи --- @@ -1066,7 +1091,7 @@ begin // "-85.3" над "dBm", снизу прижаты к верхнему краю полосы ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; ACanvas.TextOut(LblZoneL + 4, Y_BAR_TOP - TH10 - 2, LblDbm); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := TColor($00507070); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE; ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm'); // S-значение под полосой, левый край у LblZoneL+4 ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S; diff --git a/ewsdr.lpi b/ewsdr.lpi index d30731c..2e8f485 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -115,6 +115,10 @@ + + + +