From 6068d259fcd884d7146914ae7783bd521d21d6fa Mon Sep 17 00:00:00 2001 From: Danielle Date: Sat, 18 Jul 2026 11:35:03 +1000 Subject: [PATCH 1/9] Idle refresh-rate setting + copy layers from unavailable devices (v4.3.24 -> v4.3.25) Idle refresh: the disconnect throttle (surface drops to the idle tick rate while FFXIV is not attached, resumes the configured rate on attach) is now gated by a new General setting, on by default. The Settings toggle re-applies the rate immediately through RGBController.ReapplyIdleUpdateRate instead of waiting for the next connect or disconnect. Copy layers: the copy dialog now lists devices that survive only in layers.chromatics4 - provider off or hardware gone - as copy sources, alongside Mapping-tab-disabled devices. Both carry a state label in the picker ("not connected" / "disabled") and a notice under the source row so the origin is obvious, and a checkbox hides them for a shorter list. Absent devices have no IRGBDevice, so a new LayerCopier.ComputeDefaultMappingForLayer overload derives the default LedId mapping from the layer's own LedId set (identity for keyboard pairs, ordinal fallback otherwise); they stay source-only since a destination needs live LEDs. Device names aren't persisted with layers, so absent entries are labelled by device type plus an id slice. New locale keys in all seven languages. 160/160 tests pass. --- CHANGELOG.md | 5 ++ Chromatics/Chromatics.csproj | 2 +- Chromatics/Core/RGBController.cs | 15 +++- Chromatics/Helpers/LayerCopier.cs | 46 ++++++++++ Chromatics/Models/SettingsModel.cs | 4 + .../ViewModels/CopyLayersDialogViewModel.cs | 85 +++++++++++++++++-- Chromatics/ViewModels/SettingsViewModel.cs | 17 ++++ .../Views/Dialogs/CopyLayersDialog.axaml | 32 ++++--- Chromatics/Views/SettingsView.axaml | 3 + Chromatics/locale/de.json | 9 +- Chromatics/locale/en.json | 9 +- Chromatics/locale/es.json | 9 +- Chromatics/locale/fr.json | 9 +- Chromatics/locale/ja.json | 9 +- Chromatics/locale/ko.json | 9 +- Chromatics/locale/zh_CN.json | 9 +- 16 files changed, 242 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56aff85c..410be1e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Chromatics are documented here. +## 4.3.25 + +- Layers saved for a disabled or disconnected device can now be copied to another device. The copy dialog lists these devices with a label showing their state, and a checkbox hides them if you prefer a shorter list. +- New setting under Settings → General: lower the RGB refresh rate while FFXIV is not running. Enabled by default; turn it off to keep your configured refresh rate at all times. + ## 4.3.24 - **New:** Nanoleaf smart-light support (Beta). Covers the panel family (Shapes, Canvas, Elements, Lines, Aurora) and other controllers that speak the Nanoleaf OpenAPI. Enable it from Settings → Device Providers and pair each controller with a one-time button press; effects render across the panels at their real physical positions. Layer assignments stay with their panels if you add or remove panels from the wall later. Essentials bulbs and strips are not supported. diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index 16e67a91..a6e39fa0 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -10,7 +10,7 @@ net10.0-windows10.0.19041.0 10.0.17763.0 Chromatics.Program - 4.3.24.0 + 4.3.25.0 Danielle Thompson - + + + + @@ -48,7 +56,7 @@ diff --git a/Chromatics/Views/SettingsView.axaml b/Chromatics/Views/SettingsView.axaml index 55a8f5b7..cd677571 100644 --- a/Chromatics/Views/SettingsView.axaml +++ b/Chromatics/Views/SettingsView.axaml @@ -32,6 +32,9 @@ + diff --git a/Chromatics/locale/de.json b/Chromatics/locale/de.json index fff64880..d9302a5a 100644 --- a/Chromatics/locale/de.json +++ b/Chromatics/locale/de.json @@ -1001,5 +1001,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BETA] Nanoleaf-Smart-Light-Unterstützung aktivieren/deaktivieren. Koppelt Nanoleaf-Controller (Shapes, Canvas, Elements, Lines, Aurora) über Ihr LAN – jeder Controller erfordert eine einmalige Kopplung per Tastendruck. Essentials-Lampen und -Lightstrips werden nicht unterstützt. Standard: Deaktiviert", "Pairing failed. Try again.": "Kopplung fehlgeschlagen. Versuchen Sie es erneut.", "Pair": "Koppeln", - "Remove": "Entfernen" + "Remove": "Entfernen", + "Lower the RGB refresh rate while FFXIV is not running.": "Die RGB-Aktualisierungsrate verringern, während FFXIV nicht läuft.", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "Die konfigurierte Aktualisierungsrate wird wiederhergestellt, wenn Chromatics sich mit FFXIV verbindet. Standard: Aktiviert", + "Hide devices that are disabled or not connected.": "Geräte ausblenden, die deaktiviert oder nicht verbunden sind.", + "not connected": "nicht verbunden", + "disabled": "deaktiviert", + "This device is not connected. Its saved layers can still be copied.": "Dieses Gerät ist nicht verbunden. Seine gespeicherten Ebenen können weiterhin kopiert werden.", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "Dieses Gerät ist auf der Registerkarte „Zuordnungen“ deaktiviert. Seine gespeicherten Ebenen können weiterhin kopiert werden." } \ No newline at end of file diff --git a/Chromatics/locale/en.json b/Chromatics/locale/en.json index b1dfab5b..6d14ef34 100644 --- a/Chromatics/locale/en.json +++ b/Chromatics/locale/en.json @@ -1016,5 +1016,12 @@ "Remove": "Remove", "Paired - {0} panels": "Paired - {0} panels", "Nanoleaf (Beta)": "Nanoleaf (Beta)", - "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled" + "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled", + "Lower the RGB refresh rate while FFXIV is not running.": "Lower the RGB refresh rate while FFXIV is not running.", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled", + "Hide devices that are disabled or not connected.": "Hide devices that are disabled or not connected.", + "not connected": "not connected", + "disabled": "disabled", + "This device is not connected. Its saved layers can still be copied.": "This device is not connected. Its saved layers can still be copied.", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "This device is disabled on the Mappings tab. Its saved layers can still be copied." } \ No newline at end of file diff --git a/Chromatics/locale/es.json b/Chromatics/locale/es.json index 1950bf28..fc409f70 100644 --- a/Chromatics/locale/es.json +++ b/Chromatics/locale/es.json @@ -1001,5 +1001,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BETA] Activa o desactiva la compatibilidad con luces inteligentes Nanoleaf. Empareja controladores Nanoleaf (Shapes, Canvas, Elements, Lines, Aurora) a través de tu red LAN; cada controlador requiere emparejarse una sola vez pulsando un botón. Las bombillas y tiras Essentials no son compatibles. Valor predeterminado: desactivado", "Pairing failed. Try again.": "No se pudo emparejar. Inténtalo de nuevo.", "Pair": "Par", - "Remove": "Eliminar" + "Remove": "Eliminar", + "Lower the RGB refresh rate while FFXIV is not running.": "Reduce la frecuencia de actualización RGB mientras FFXIV no se esté ejecutando.", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "La frecuencia de actualización configurada se reanuda cuando Chromatics se conecta a FFXIV. Predeterminado: activado", + "Hide devices that are disabled or not connected.": "Oculta los dispositivos que estén desactivados o no conectados.", + "not connected": "no conectado", + "disabled": "desactivado", + "This device is not connected. Its saved layers can still be copied.": "Este dispositivo no está conectado. Sus capas guardadas aún se pueden copiar.", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "Este dispositivo está desactivado en la pestaña Asignaciones. Sus capas guardadas aún se pueden copiar." } \ No newline at end of file diff --git a/Chromatics/locale/fr.json b/Chromatics/locale/fr.json index edf37dab..80875d26 100644 --- a/Chromatics/locale/fr.json +++ b/Chromatics/locale/fr.json @@ -1001,5 +1001,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BÊTA] Active/désactive la prise en charge des éclairages connectés Nanoleaf. Associe les contrôleurs Nanoleaf (Shapes, Canvas, Elements, Lines, Aurora) via votre réseau local ; chaque contrôleur nécessite une association unique par pression sur un bouton. Les ampoules et rubans Essentials ne sont pas pris en charge. Valeur par défaut : désactivé", "Pairing failed. Try again.": "Échec de l’appairage. Réessayez.", "Pair": "Paire", - "Remove": "Supprimer" + "Remove": "Supprimer", + "Lower the RGB refresh rate while FFXIV is not running.": "Réduire le taux de rafraîchissement RVB lorsque FFXIV n’est pas en cours d’exécution.", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "Le taux de rafraîchissement configuré reprend lorsque Chromatics se connecte à FFXIV. Par défaut : activé", + "Hide devices that are disabled or not connected.": "Masquer les périphériques désactivés ou non connectés.", + "not connected": "non connecté", + "disabled": "désactivé", + "This device is not connected. Its saved layers can still be copied.": "Ce périphérique n’est pas connecté. Ses calques enregistrés peuvent toujours être copiés.", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "Ce périphérique est désactivé dans l’onglet Mappages. Ses calques enregistrés peuvent toujours être copiés." } \ No newline at end of file diff --git a/Chromatics/locale/ja.json b/Chromatics/locale/ja.json index 7fd05e93..16616aca 100644 --- a/Chromatics/locale/ja.json +++ b/Chromatics/locale/ja.json @@ -1002,5 +1002,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[ベータ] Nanoleaf スマートライトのサポートを有効/無効にします。LAN 経由で Nanoleaf コントローラー(Shapes、Canvas、Elements、Lines、Aurora)をペアリングします。各コントローラーでは初回のみボタン押下によるペアリングが必要です。Essentials の電球およびライトストリップには対応していません。デフォルト: 無効", "Pairing failed. Try again.": "ペアリングに失敗しました。もう一度お試しください。", "Pair": "ペア", - "Remove": "削除" + "Remove": "削除", + "Lower the RGB refresh rate while FFXIV is not running.": "FFXIVが実行されていない間、RGBのリフレッシュレートを下げます。", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "ChromaticsがFFXIVに接続すると、設定されたリフレッシュレートに戻ります。デフォルト:有効", + "Hide devices that are disabled or not connected.": "無効化されている、または接続されていないデバイスを非表示にします。", + "not connected": "未接続", + "disabled": "無効", + "This device is not connected. Its saved layers can still be copied.": "このデバイスは接続されていません。保存済みのレイヤーは引き続きコピーできます。", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "このデバイスは「マッピング」タブで無効化されています。保存済みのレイヤーは引き続きコピーできます。" } \ No newline at end of file diff --git a/Chromatics/locale/ko.json b/Chromatics/locale/ko.json index a1062640..5cb3c467 100644 --- a/Chromatics/locale/ko.json +++ b/Chromatics/locale/ko.json @@ -1002,5 +1002,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[베타] Nanoleaf 스마트 조명 지원을 활성화/비활성화합니다. LAN을 통해 Nanoleaf 컨트롤러(Shapes, Canvas, Elements, Lines, Aurora)를 페어링합니다. 각 컨트롤러는 한 번의 버튼 누름 페어링이 필요합니다. Essentials 전구 및 스트립은 지원되지 않습니다. 기본값: 비활성화", "Pairing failed. Try again.": "페어링에 실패했습니다. 다시 시도하세요.", "Pair": "페어링", - "Remove": "제거" + "Remove": "제거", + "Lower the RGB refresh rate while FFXIV is not running.": "FFXIV가 실행 중이 아닐 때 RGB 새로 고침 빈도를 낮춥니다.", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "Chromatics가 FFXIV에 연결되면 설정된 새로 고침 빈도로 돌아갑니다. 기본값: 활성화됨", + "Hide devices that are disabled or not connected.": "비활성화되었거나 연결되지 않은 장치를 숨깁니다.", + "not connected": "연결되지 않음", + "disabled": "비활성화됨", + "This device is not connected. Its saved layers can still be copied.": "이 장치는 연결되어 있지 않습니다. 저장된 레이어는 계속 복사할 수 있습니다.", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "이 장치는 매핑 탭에서 비활성화되어 있습니다. 저장된 레이어는 계속 복사할 수 있습니다." } \ No newline at end of file diff --git a/Chromatics/locale/zh_CN.json b/Chromatics/locale/zh_CN.json index 5378197d..70d1c405 100644 --- a/Chromatics/locale/zh_CN.json +++ b/Chromatics/locale/zh_CN.json @@ -1001,5 +1001,12 @@ "[BETA] Enable/disable Nanoleaf smart-light support. Pairs Nanoleaf controllers (Shapes, Canvas, Elements, Lines, Aurora) over your LAN - each controller needs a one-time button-press pairing. Essentials bulbs and strips are not supported. Default: Disabled": "[BETA] 启用/禁用 Nanoleaf 智能灯支持。通过局域网配对 Nanoleaf 控制器(Shapes、Canvas、Elements、Lines、Aurora)——每个控制器都需要一次性按键配对。不支持 Essentials 灯泡和灯带。默认:禁用", "Pairing failed. Try again.": "配对失败。请重试。", "Pair": "配对", - "Remove": "删除" + "Remove": "删除", + "Lower the RGB refresh rate while FFXIV is not running.": "在 FFXIV 未运行时降低 RGB 刷新率。", + "The configured refresh rate resumes when Chromatics attaches to FFXIV. Default: Enabled": "当 Chromatics 连接到 FFXIV 时,将恢复已配置的刷新率。默认:已启用", + "Hide devices that are disabled or not connected.": "隐藏已禁用或未连接的设备。", + "not connected": "未连接", + "disabled": "已禁用", + "This device is not connected. Its saved layers can still be copied.": "此设备未连接。仍可复制其已保存的图层。", + "This device is disabled on the Mappings tab. Its saved layers can still be copied.": "此设备已在“映射”选项卡中禁用。仍可复制其已保存的图层。" } \ No newline at end of file From 084c0748c14f5f82a8fc20eca980671ba1ee46ce Mon Sep 17 00:00:00 2001 From: Danielle Date: Sat, 18 Jul 2026 11:45:50 +1000 Subject: [PATCH 2/9] PR #206 review fixes: destination list follows the hide filter, mapping rules under test (v4.3.25 -> v4.3.26) The hide checkbox only rebuilt the source list: when the selected source survived the filter, the selection setter no-oped, the source-changed handler never ran, and the destination dropdown kept (or kept missing) disabled devices until the source changed. The filter toggle now rebuilds destinations, revalidates the selected destination, and rebuilds the layer rows whenever the source selection carries over. The absent-source default-mapping rules moved onto a device-free core (source LedIds + destination LedId list) with the IRGBDevice overload delegating to it, since constructing an IRGBDevice needs a full device stack that kept the rules out of unit-test reach. Five tests pin the behaviour: keyboard identity with no positional fallback, exact-id match, ordinal fallback, tail dropping, and empty-input handling. 165/165 tests pass. --- CHANGELOG.md | 6 +- Chromatics.Tests/Helpers/LayerCopierTests.cs | 80 +++++++++++++++++++ Chromatics/Chromatics.csproj | 2 +- Chromatics/Helpers/LayerCopier.cs | 29 +++++-- .../ViewModels/CopyLayersDialogViewModel.cs | 21 ++++- 5 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 Chromatics.Tests/Helpers/LayerCopierTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 410be1e1..dbcfe51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ All notable changes to Chromatics are documented here. -## 4.3.25 +## 4.3.26 -- Layers saved for a disabled or disconnected device can now be copied to another device. The copy dialog lists these devices with a label showing their state, and a checkbox hides them if you prefer a shorter list. -- New setting under Settings → General: lower the RGB refresh rate while FFXIV is not running. Enabled by default; turn it off to keep your configured refresh rate at all times. +- Layers saved for a disabled or disconnected devices can now be copied to another device. +- New setting under Settings → General: lower the RGB refresh rate while FFXIV is not running. ## 4.3.24 diff --git a/Chromatics.Tests/Helpers/LayerCopierTests.cs b/Chromatics.Tests/Helpers/LayerCopierTests.cs new file mode 100644 index 00000000..d881113a --- /dev/null +++ b/Chromatics.Tests/Helpers/LayerCopierTests.cs @@ -0,0 +1,80 @@ +using Chromatics.Helpers; +using RGB.NET.Core; + +namespace Chromatics.Tests.Helpers; + +// Pins the default LedId mapping used when copying layers from a device +// that is disabled or no longer connected: the layer's own LedId set +// stands in for the missing device, so these rules decide where each key +// lands on the destination before the user touches the override dropdowns. +public class LayerCopierTests +{ + [Fact] + public void KeyboardPair_MapsIdentity_OnlyWhereDestinationHasTheKey() + { + var used = new[] { LedId.Keyboard_A, LedId.Keyboard_B, LedId.Keyboard_NumLock }; + var destIds = new List { LedId.Keyboard_A, LedId.Keyboard_B }; // no numpad + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, destIds); + + Assert.Equal(LedId.Keyboard_A, map[LedId.Keyboard_A]); + Assert.Equal(LedId.Keyboard_B, map[LedId.Keyboard_B]); + // Keyboard pairs never fall back positionally: a missing key is + // dropped so it can't land on an unrelated physical key. + Assert.False(map.ContainsKey(LedId.Keyboard_NumLock)); + } + + [Fact] + public void NonKeyboard_ExactIdMatchWins() + { + var used = new[] { LedId.Custom1, LedId.Custom2 }; + var destIds = new List { LedId.Custom1, LedId.Custom2, LedId.Custom3 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.LedStripe, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom1]); + Assert.Equal(LedId.Custom2, map[LedId.Custom2]); + } + + [Fact] + public void NonKeyboard_OrdinalFallback_WhenIdsDoNotOverlap() + { + // Source painted Custom5..Custom7; destination only has Custom1..Custom3. + var used = new[] { LedId.Custom5, LedId.Custom6, LedId.Custom7 }; + var destIds = new List { LedId.Custom1, LedId.Custom2, LedId.Custom3 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.Mouse, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom5]); + Assert.Equal(LedId.Custom2, map[LedId.Custom6]); + Assert.Equal(LedId.Custom3, map[LedId.Custom7]); + } + + [Fact] + public void NonKeyboard_SourceLargerThanDestination_DropsTheTail() + { + var used = new[] { LedId.Custom5, LedId.Custom6, LedId.Custom7 }; + var destIds = new List { LedId.Custom1 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.LedStripe, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom5]); + Assert.False(map.ContainsKey(LedId.Custom6)); + Assert.False(map.ContainsKey(LedId.Custom7)); + } + + [Fact] + public void EmptyOrMissingInputs_ReturnEmptyMap() + { + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + Array.Empty(), RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, new List { LedId.Keyboard_A })); + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + null, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, new List())); + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + new[] { LedId.Keyboard_A }, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, null)); + } +} diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index a6e39fa0..3eac75a2 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -10,7 +10,7 @@ net10.0-windows10.0.19041.0 10.0.17763.0 Chromatics.Program - 4.3.25.0 + 4.3.26.0 Danielle Thompson