From f1ade495fbf6be3e9d74c7b619a68c7248735eec Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Wed, 4 Mar 2026 20:32:34 +0100 Subject: [PATCH] Implement Component Groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component groups allow nesting components in the opposite layout direction to their parent, enabling horizontal rows inside vertical layouts and vice versa. Groups can be nested arbitrarily deep, with the direction alternating at each level. The layout editor presents all components, including those nested inside groups, in a single flat list with indentation levels. Empty groups show placeholder entries. Adding, removing, moving, and duplicating components all work across group boundaries. The editor state exposes the layout direction at the selected position. Changelog (en): The layout editor now supports adding rows and columns of components, allowing for much more complex layouts. Changelog (de): Der Layout-Editor unterstützt jetzt das Hinzufügen von Zeilen und Spalten von Komponenten, was deutlich komplexere Layouts ermöglicht. Changelog (fr): L'éditeur de layout permet désormais d'ajouter des lignes et des colonnes de composants, permettant des mises en page bien plus complexes. Changelog (nl): De lay-outeditor ondersteunt nu het toevoegen van rijen en kolommen van componenten, waardoor veel complexere lay-outs mogelijk zijn. Changelog (es): El editor de diseño ahora permite añadir filas y columnas de componentes, lo que posibilita diseños mucho más complejos. Changelog (it): L'editor del layout ora supporta l'aggiunta di righe e colonne di componenti, consentendo layout molto più complessi. Changelog (pt): O editor de layout agora suporta adicionar linhas e colunas de componentes, permitindo layouts muito mais complexos. Changelog (pt-BR): O editor de layout agora suporta adicionar linhas e colunas de componentes, permitindo layouts muito mais complexos. Changelog (pl): Edytor układu obsługuje teraz dodawanie wierszy i kolumn komponentów, co umożliwia tworzenie znacznie bardziej złożonych układów. Changelog (ru): Редактор макета теперь поддерживает добавление строк и столбцов компонентов, что позволяет создавать гораздо более сложные макеты. Changelog (ja): レイアウトエディタでコンポーネントの行と列を追加できるようになり、より複雑なレイアウトが可能になりました。 Changelog (ko): 레이아웃 편집기에서 구성 요소의 행과 열을 추가할 수 있게 되어, 훨씬 더 복잡한 레이아웃을 만들 수 있습니다. Changelog (zh-Hans): 布局编辑器现在支持添加组件的行和列,从而实现更加复杂的布局。 Changelog (zh-Hant): 佈局編輯器現在支持添加組件的行和列,從而實現更加複雜的佈局。 --- livesplit-core | 2 +- src/css/LayoutEditor.module.css | 5 +++ src/localization/chinese-simplified.ts | 4 ++ src/localization/dutch.ts | 4 ++ src/localization/english.ts | 4 ++ src/localization/french.ts | 4 ++ src/localization/german.ts | 4 ++ src/localization/index.ts | 4 ++ src/localization/italian.ts | 4 ++ src/localization/japanese.ts | 4 ++ src/localization/korean.ts | 4 ++ src/localization/polish.ts | 4 ++ src/localization/portuguese-brazil.ts | 4 ++ src/localization/portuguese.ts | 4 ++ src/localization/russian.ts | 4 ++ src/localization/spanish.ts | 4 ++ src/ui/components/Settings/index.tsx | 54 ++++++++++++++++++++++++++ src/ui/views/LayoutEditor.tsx | 39 ++++++++++++++++++- 18 files changed, 154 insertions(+), 2 deletions(-) diff --git a/livesplit-core b/livesplit-core index d6411401..c4a2068b 160000 --- a/livesplit-core +++ b/livesplit-core @@ -1 +1 @@ -Subproject commit d64114013d140b1b6fb581d7d640136af162a43a +Subproject commit c4a2068b71586d92d3864de00249226c88edf774 diff --git a/src/css/LayoutEditor.module.css b/src/css/LayoutEditor.module.css index 388c49a4..2f7e7c01 100644 --- a/src/css/LayoutEditor.module.css +++ b/src/css/LayoutEditor.module.css @@ -56,6 +56,11 @@ cursor: pointer; } +.placeholder { + font-style: italic; + opacity: 0.5; +} + .layoutContainer { margin-left: var(--ui-large-margin); diff --git a/src/localization/chinese-simplified.ts b/src/localization/chinese-simplified.ts index a5f91fff..0052a86e 100644 --- a/src/localization/chinese-simplified.ts +++ b/src/localization/chinese-simplified.ts @@ -247,6 +247,10 @@ export function resolveChineseSimplified(text: Label): string { case Label.ComponentBlankSpaceDescription: return "仅显示背景的空组件,用作间距。"; case Label.ComponentSeparator: return "分隔线"; case Label.ComponentSeparatorDescription: return "在组件之间渲染分隔线。"; + case Label.Row: return "行"; + case Label.RowDescription: return "一行水平排列的组件,改变内部组件的布局方向。"; + case Label.Column: return "列"; + case Label.ColumnDescription: return "一列垂直排列的组件,改变内部组件的布局方向。"; case Label.AccuracySeconds: return "秒"; case Label.AccuracyTenths: return "十分之一秒"; case Label.AccuracyHundredths: return "百分之一秒"; diff --git a/src/localization/dutch.ts b/src/localization/dutch.ts index e931ccae..64392682 100644 --- a/src/localization/dutch.ts +++ b/src/localization/dutch.ts @@ -247,6 +247,10 @@ export function resolveDutch(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Leeg component voor padding tussen onderdelen."; case Label.ComponentSeparator: return "Scheiding"; case Label.ComponentSeparatorDescription: return "Tekent een scheidingslijn tussen componenten."; + case Label.Row: return "Rij"; + case Label.RowDescription: return "Een rij componenten die horizontaal zijn ingedeeld, waardoor de lay-outrichting voor de componenten binnenin verandert."; + case Label.Column: return "Kolom"; + case Label.ColumnDescription: return "Een kolom componenten die verticaal zijn ingedeeld, waardoor de lay-outrichting voor de componenten binnenin verandert."; case Label.AccuracySeconds: return "Seconden"; case Label.AccuracyTenths: return "Tienden"; case Label.AccuracyHundredths: return "Honderdsten"; diff --git a/src/localization/english.ts b/src/localization/english.ts index 6dcd0be4..3dabb293 100644 --- a/src/localization/english.ts +++ b/src/localization/english.ts @@ -247,6 +247,10 @@ export function resolveEnglish(text: Label): string { case Label.ComponentBlankSpaceDescription: return "An empty component that doesn't show anything other than a background. It mostly serves as padding between other components."; case Label.ComponentSeparator: return "Separator"; case Label.ComponentSeparatorDescription: return "A simple component that just renders a separator between components."; + case Label.Row: return "Row"; + case Label.RowDescription: return "A row of components laid out horizontally, changing the layout direction for the components inside."; + case Label.Column: return "Column"; + case Label.ColumnDescription: return "A column of components laid out vertically, changing the layout direction for the components inside."; case Label.AccuracySeconds: return "Seconds"; case Label.AccuracyTenths: return "Tenths"; case Label.AccuracyHundredths: return "Hundredths"; diff --git a/src/localization/french.ts b/src/localization/french.ts index 3fd1595b..105b4574 100644 --- a/src/localization/french.ts +++ b/src/localization/french.ts @@ -247,6 +247,10 @@ export function resolveFrench(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Composant vide servant d’espacement."; case Label.ComponentSeparator: return "Séparateur"; case Label.ComponentSeparatorDescription: return "Affiche un séparateur entre les composants."; + case Label.Row: return "Ligne"; + case Label.RowDescription: return "Une ligne de composants disposés horizontalement, changeant la direction du layout pour les composants à l'intérieur."; + case Label.Column: return "Colonne"; + case Label.ColumnDescription: return "Une colonne de composants disposés verticalement, changeant la direction du layout pour les composants à l'intérieur."; case Label.AccuracySeconds: return "Secondes"; case Label.AccuracyTenths: return "Dixièmes"; case Label.AccuracyHundredths: return "Centièmes"; diff --git a/src/localization/german.ts b/src/localization/german.ts index 069bc53d..668099bf 100644 --- a/src/localization/german.ts +++ b/src/localization/german.ts @@ -247,6 +247,10 @@ export function resolveGerman(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Leere Komponente als Abstand zwischen anderen."; case Label.ComponentSeparator: return "Trenner"; case Label.ComponentSeparatorDescription: return "Zeichnet einen Trenner zwischen Komponenten."; + case Label.Row: return "Zeile"; + case Label.RowDescription: return "Eine Zeile von Komponenten, die horizontal angeordnet sind und die Layoutrichtung für die enthaltenen Komponenten ändern."; + case Label.Column: return "Spalte"; + case Label.ColumnDescription: return "Eine Spalte von Komponenten, die vertikal angeordnet sind und die Layoutrichtung für die enthaltenen Komponenten ändern."; case Label.AccuracySeconds: return "Sekunden"; case Label.AccuracyTenths: return "Zehntel"; case Label.AccuracyHundredths: return "Hundertstel"; diff --git a/src/localization/index.ts b/src/localization/index.ts index 1e804ea7..4ad9ddd6 100644 --- a/src/localization/index.ts +++ b/src/localization/index.ts @@ -260,6 +260,10 @@ export enum Label { ComponentBlankSpaceDescription, ComponentSeparator, ComponentSeparatorDescription, + Row, + RowDescription, + Column, + ColumnDescription, AccuracySeconds, AccuracyTenths, AccuracyHundredths, diff --git a/src/localization/italian.ts b/src/localization/italian.ts index 099e1aee..45b51aeb 100644 --- a/src/localization/italian.ts +++ b/src/localization/italian.ts @@ -247,6 +247,10 @@ export function resolveItalian(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Componente vuoto usato come spaziatura."; case Label.ComponentSeparator: return "Separatore"; case Label.ComponentSeparatorDescription: return "Rende un separatore tra componenti."; + case Label.Row: return "Riga"; + case Label.RowDescription: return "Una riga di componenti disposti orizzontalmente, che cambia la direzione del layout per i componenti al suo interno."; + case Label.Column: return "Colonna"; + case Label.ColumnDescription: return "Una colonna di componenti disposti verticalmente, che cambia la direzione del layout per i componenti al suo interno."; case Label.AccuracySeconds: return "Secondi"; case Label.AccuracyTenths: return "Decimi"; case Label.AccuracyHundredths: return "Centesimi"; diff --git a/src/localization/japanese.ts b/src/localization/japanese.ts index c2b68b98..8b7b2493 100644 --- a/src/localization/japanese.ts +++ b/src/localization/japanese.ts @@ -247,6 +247,10 @@ export function resolveJapanese(text: Label): string { case Label.ComponentBlankSpaceDescription: return "背景のみの空コンポーネントです。コンポーネント間の余白として使います。"; case Label.ComponentSeparator: return "区切り"; case Label.ComponentSeparatorDescription: return "コンポーネント間の区切り線を表示します。"; + case Label.Row: return "行"; + case Label.RowDescription: return "水平に配置されたコンポーネントの行で、内部のコンポーネントのレイアウト方向を変更します。"; + case Label.Column: return "列"; + case Label.ColumnDescription: return "垂直に配置されたコンポーネントの列で、内部のコンポーネントのレイアウト方向を変更します。"; case Label.AccuracySeconds: return "秒"; case Label.AccuracyTenths: return "1/10 秒"; case Label.AccuracyHundredths: return "1/100 秒"; diff --git a/src/localization/korean.ts b/src/localization/korean.ts index bf333fed..e7f46a2b 100644 --- a/src/localization/korean.ts +++ b/src/localization/korean.ts @@ -247,6 +247,10 @@ export function resolveKorean(text: Label): string { case Label.ComponentBlankSpaceDescription: return "배경만 있는 빈 컴포넌트입니다."; case Label.ComponentSeparator: return "구분선"; case Label.ComponentSeparatorDescription: return "컴포넌트 사이의 구분선을 표시합니다."; + case Label.Row: return "행"; + case Label.RowDescription: return "수평으로 배치된 구성 요소의 행으로, 내부 구성 요소의 레이아웃 방향을 변경합니다."; + case Label.Column: return "열"; + case Label.ColumnDescription: return "수직으로 배치된 구성 요소의 열로, 내부 구성 요소의 레이아웃 방향을 변경합니다."; case Label.AccuracySeconds: return "초"; case Label.AccuracyTenths: return "0.1초"; case Label.AccuracyHundredths: return "0.01초"; diff --git a/src/localization/polish.ts b/src/localization/polish.ts index b2e08c4a..39b810a0 100644 --- a/src/localization/polish.ts +++ b/src/localization/polish.ts @@ -247,6 +247,10 @@ export function resolvePolish(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Pusty komponent, który nie pokazuje nic poza tłem. Służy głównie jako odstęp między innymi komponentami."; case Label.ComponentSeparator: return "Separator"; case Label.ComponentSeparatorDescription: return "Prosty komponent, który renderuje separator między komponentami."; + case Label.Row: return "Wiersz"; + case Label.RowDescription: return "Wiersz komponentów ułożonych poziomo, zmieniający kierunek układu dla komponentów wewnątrz."; + case Label.Column: return "Kolumna"; + case Label.ColumnDescription: return "Kolumna komponentów ułożonych pionowo, zmieniająca kierunek układu dla komponentów wewnątrz."; case Label.AccuracySeconds: return "Sekundy"; case Label.AccuracyTenths: return "Dziesiąte"; case Label.AccuracyHundredths: return "Setne"; diff --git a/src/localization/portuguese-brazil.ts b/src/localization/portuguese-brazil.ts index 78df60b7..ecd6dc92 100644 --- a/src/localization/portuguese-brazil.ts +++ b/src/localization/portuguese-brazil.ts @@ -247,6 +247,10 @@ export function resolveBrazilianPortuguese(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Componente vazio com apenas o fundo."; case Label.ComponentSeparator: return "Separador"; case Label.ComponentSeparatorDescription: return "Mostra uma linha separadora entre componentes."; + case Label.Row: return "Linha"; + case Label.RowDescription: return "Uma linha de componentes dispostos horizontalmente, alterando a direção do layout para os componentes internos."; + case Label.Column: return "Coluna"; + case Label.ColumnDescription: return "Uma coluna de componentes dispostos verticalmente, alterando a direção do layout para os componentes internos."; case Label.AccuracySeconds: return "Segundos"; case Label.AccuracyTenths: return "Décimos"; case Label.AccuracyHundredths: return "Centésimos"; diff --git a/src/localization/portuguese.ts b/src/localization/portuguese.ts index b5d5ef52..72aee779 100644 --- a/src/localization/portuguese.ts +++ b/src/localization/portuguese.ts @@ -247,6 +247,10 @@ export function resolvePortuguese(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Componente vazio com apenas o fundo."; case Label.ComponentSeparator: return "Separador"; case Label.ComponentSeparatorDescription: return "Mostra uma linha separadora entre componentes."; + case Label.Row: return "Linha"; + case Label.RowDescription: return "Uma linha de componentes dispostos horizontalmente, alterando a direção do layout para os componentes no interior."; + case Label.Column: return "Coluna"; + case Label.ColumnDescription: return "Uma coluna de componentes dispostos verticalmente, alterando a direção do layout para os componentes no interior."; case Label.AccuracySeconds: return "Segundos"; case Label.AccuracyTenths: return "Décimos"; case Label.AccuracyHundredths: return "Centésimos"; diff --git a/src/localization/russian.ts b/src/localization/russian.ts index a9db57bb..bd553ba8 100644 --- a/src/localization/russian.ts +++ b/src/localization/russian.ts @@ -247,6 +247,10 @@ export function resolveRussian(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Пустой компонент только с фоном."; case Label.ComponentSeparator: return "Разделитель"; case Label.ComponentSeparatorDescription: return "Показывает разделительную линию между компонентами."; + case Label.Row: return "Строка"; + case Label.RowDescription: return "Строка компонентов, расположенных горизонтально, изменяющая направление разметки для компонентов внутри."; + case Label.Column: return "Столбец"; + case Label.ColumnDescription: return "Столбец компонентов, расположенных вертикально, изменяющий направление разметки для компонентов внутри."; case Label.AccuracySeconds: return "Секунды"; case Label.AccuracyTenths: return "Десятые"; case Label.AccuracyHundredths: return "Сотые"; diff --git a/src/localization/spanish.ts b/src/localization/spanish.ts index 33a588cd..9c1d983b 100644 --- a/src/localization/spanish.ts +++ b/src/localization/spanish.ts @@ -247,6 +247,10 @@ export function resolveSpanish(text: Label): string { case Label.ComponentBlankSpaceDescription: return "Componente vacío con solo el fondo."; case Label.ComponentSeparator: return "Separador"; case Label.ComponentSeparatorDescription: return "Muestra una línea separadora entre componentes."; + case Label.Row: return "Fila"; + case Label.RowDescription: return "Una fila de componentes dispuestos horizontalmente, cambiando la dirección del diseño para los componentes interiores."; + case Label.Column: return "Columna"; + case Label.ColumnDescription: return "Una columna de componentes dispuestos verticalmente, cambiando la dirección del diseño para los componentes interiores."; case Label.AccuracySeconds: return "Segundos"; case Label.AccuracyTenths: return "Décimas"; case Label.AccuracyHundredths: return "Centésimas"; diff --git a/src/ui/components/Settings/index.tsx b/src/ui/components/Settings/index.tsx index fb20e659..97b77d90 100644 --- a/src/ui/components/Settings/index.tsx +++ b/src/ui/components/Settings/index.tsx @@ -73,6 +73,8 @@ export type ExtendedSettingsDescriptionValueJson = export interface SettingValueFactory { fromBool(v: boolean): T; fromUint(value: number): T; + fromOptionalUint(value: number): T; + fromOptionalEmptyUint(): T; fromInt(value: number): T; fromString(value: string): T; fromOptionalString(value: string): T; @@ -146,6 +148,12 @@ export class JsonSettingValueFactory implements SettingValueFactory extends React.Component> { /> ); + } else if ("OptionalUInt" in value) { + const children = [ + { + if (checked) { + this.props.setValue( + valueIndex, + factory.fromOptionalUint(1), + ); + } else { + this.props.setValue( + valueIndex, + factory.fromOptionalEmptyUint(), + ); + } + }} + />, + ]; + + if (value.OptionalUInt !== null) { + children.push( + { + this.props.setValue( + valueIndex, + factory.fromOptionalUint( + e.target.valueAsNumber, + ), + ); + }} + />, + ); + } + + component = ( +
+ {children} +
+ ); } else if ("Int" in value) { component = (
diff --git a/src/ui/views/LayoutEditor.tsx b/src/ui/views/LayoutEditor.tsx index bb5a044e..2560d0ce 100644 --- a/src/ui/views/LayoutEditor.tsx +++ b/src/ui/views/LayoutEditor.tsx @@ -134,6 +134,8 @@ export function View({ if (i === state.selected_component) { className += " " + tableClasses.selected; } + const indent = state.indent_levels[i]; + const isPlaceholder = state.is_placeholder[i]; return ( - {c} + + + {c} + + ); }); @@ -207,6 +220,7 @@ export function View({ allVariables={allVariables} addVariable={(v) => addVariable(v)} addComponent={(v) => addComponent(v)} + layoutDirection={state.layout_direction} lang={lang} /> @@ -324,11 +339,13 @@ function AddComponentButton({ allVariables, addVariable, addComponent, + layoutDirection, lang, }: { allVariables: Set; addVariable: (name: string) => void; addComponent: (componentClass: ComponentClass) => void; + layoutDirection: LiveSplit.LayoutDirection; lang?: LiveSplit.Language; }) { const [position, setPosition] = useState(null); @@ -583,6 +600,26 @@ function AddComponentButton({ )} + addComponent(LiveSplit.GroupComponent)} + lang={lang} + > + {resolve( + layoutDirection === "Vertical" + ? Label.Row + : Label.Column, + lang, + )} + + {resolve( + layoutDirection === "Vertical" + ? Label.RowDescription + : Label.ColumnDescription, + lang, + )} + +