Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
### Fixes
- **Xbox controllers no longer ask to be configured** (#284): when an Xbox controller also showed up as a "joystick without a profile" (a second instance, a receiver or a generic driver), the app said "Controle sem perfil… hold any button" and opened the mapping wizard even though the controller already worked. Xbox controllers (Microsoft vendor, or "Xbox"/"XInput" in the name) now never trigger the notice or the wizard; real generic joysticks can still be configured.
### Changes
- **Landscape Settings, easier to find** (#285, #288, #227): Configurações now uses the screen's width: groups sit side by side in two columns (Exibição and Busca on the left; Controles and ControlFS on the right, in reading order: Down at the end of the first column continues at the top of the second), with the description below and no scrolling at 720p. In a narrow window it goes back to one column. **L1/R1** jump between groups. In the Menu, **Configurações** is now the **first tile** (already focused: Menu + Confirm opens it), with the gear and what's inside. No setting was removed or changed behavior.
- **Controller sounds on by default and nicer** (#286, #287): a fresh install (or no saved choice) now has the sounds on at medium volume; anyone who picked "desligados" (off) stays silent, and the choice is kept across restarts and updates. The four cues were redone: glassy timbre (a fundamental with soft harmonics), rounded attack, gentle decay, all at the same loudness and without clicks; "baixo" (low) is now really low. They are still synthesized in the app (no third-party files; origin and license in THIRD_PARTY_NOTICES.md).
- **Git status left Configurações** (#284): the option, the "Status do Git: ligado/desligado" notice, the branch in the header and the "Git: modificado" marks are gone from the interface.
### What's new
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
### Correções
- **Controle Xbox não pede mais para ser configurado** (#284): quando um controle Xbox aparecia também como "joystick sem perfil" (segunda instância, receptor ou driver genérico), o app avisava "Controle sem perfil… segure qualquer botão" e abria o assistente de configuração, mesmo com o controle já funcionando. Agora controles Xbox (fabricante Microsoft, ou "Xbox"/"XInput" no nome) nunca disparam o aviso nem o assistente; joysticks genéricos de verdade continuam podendo ser configurados.
### Mudanças
- **Configurações em paisagem e mais fáceis de achar** (#285, #288, #227): as Configurações agora ocupam a largura da tela: os grupos ficam lado a lado em duas colunas (Exibição e Busca à esquerda; Controles e ControlFS à direita, em ordem de leitura: Baixo no fim da primeira coluna continua no topo da segunda), com descrição embaixo, sem rolar em 720p. Em janela estreita voltam a uma coluna. **L1/R1** pulam de um grupo para outro. No Menu, **Configurações** agora é o **primeiro bloco** (já em foco: Menu + Confirmar abre), com a engrenagem e o que tem dentro. Nenhum ajuste foi removido ou mudou de comportamento.
- **Sons do controle ligados por padrão e mais bonitos** (#286, #287): numa instalação nova (ou sem escolha salva) os sons já vêm ligados em volume médio; quem escolheu "desligados" continua sem som, e a escolha fica salva ao reabrir e ao atualizar. Os quatro sons foram refeitos: timbre de vidro (fundamental com harmônicos suaves), ataque arredondado, queda suave, todos com o mesmo volume e sem estalos; o volume "baixo" agora é realmente baixo. Continuam sintetizados no app (sem arquivos de terceiros; a origem e a licença estão em THIRD_PARTY_NOTICES.md).
- **Status do Git saiu das Configurações** (#284): a opção, o aviso "Status do Git: ligado/desligado", o ramo no cabeçalho e as marcas "Git: modificado" deixaram de existir na interface.
### Novidades
Expand Down
2 changes: 1 addition & 1 deletion docs/ui-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ added line.
| Where | Pickers | Stays one press |
|---|---|---|
| Compactar | Formato, Compressão | Nome (keyboard), Compactar |
| Configurações | Exibição, Densidade da lista, Tema, Cor de destaque, Ordenar por, Confirmar com, Legendas, Fluidez | Ordem, Painel de detalhes, Itens ocultos, Tela cheia, Busca em subpastas, Recentes, Restaurar abas, Sugestões do teclado, Segundo plano, Mira por giroscópio (on/off or symmetric two-way toggles); Controle ativo, Teste de controles, Controles sem perfil, Atualizações (they open their own screen) |
| Configurações (paisagem, 2 colunas; L1/R1 pulam de grupo; primeiro bloco do Menu) | Exibição, Densidade da lista, Tema, Cor de destaque, Ordenar por, Confirmar com, Legendas, Fluidez | Ordem, Painel de detalhes, Itens ocultos, Tela cheia, Busca em subpastas, Recentes, Restaurar abas, Sugestões do teclado, Segundo plano, Mira por giroscópio (on/off or symmetric two-way toggles); Controle ativo, Teste de controles, Controles sem perfil, Atualizações (they open their own screen) |
| Filtros da busca | Tamanho, Modificado | Tipos (checkboxes) |
| Renomear em lote | Modo, Converter para | Dígitos (1–6, a stepper), text fields |

Expand Down
36 changes: 30 additions & 6 deletions src/ControlFS.App/Views/ModalView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ public static void ScrollBody(int steps)

private static double PanelMargin => Theme.SpaceM;

/// <summary>Largura mínima do painel (px lógicos) para as Configurações ficarem em duas colunas (#285).</summary>
private const double MenuTwoColumnMinWidth = 900;

/// <summary>
/// Largura fixa de cada classe de tamanho (#227, px lógicos antes da escala). Nunca depende do conteúdo em foco; a janela
/// (720p, portátil, 4K) sempre limita (<see cref="PanelWidthFor"/>).
Expand All @@ -171,6 +174,7 @@ public static void ScrollBody(int steps)
ModalSize.Medium => 540,
ModalSize.Standard => 640,
ModalSize.Wide => 960,
ModalSize.Landscape => 1080,
_ => double.PositiveInfinity,
};

Expand Down Expand Up @@ -607,6 +611,13 @@ private static StackPanel SectionHeading(string? title, bool first)
private static Border BuildMenu(AppController app, MenuModal menu, out Action update)
{
var stack = new StackPanel();
// Configurações em paisagem (#285): com largura para isso, os grupos ficam em duas colunas lado a lado (a de baixo
// continua a de cima em ordem de leitura). Numa janela estreita, uma coluna só, como sempre.
var twoColumns = menu.HasSectionGrids && PanelWidthFor(menu.Size) >= Theme.Scaled(MenuTwoColumnMinWidth);
var second = twoColumns ? new StackPanel() : null;
var split = twoColumns ? menu.ColumnSplit : int.MaxValue;
var target = stack;
var owners = new StackPanel[menu.Items.Count];
var hasGrid = menu.Grids.Count > 0;
// Títulos de grupo: em menus só de lista e nas Configurações; com a grade de ações rápidas no topo, só o fio (como o
// menu de contexto do Windows 11).
Expand All @@ -616,29 +627,42 @@ private static Border BuildMenu(AppController app, MenuModal menu, out Action up
string? section = null;
for (var i = 0; i < menu.Items.Count;)
{
if (i >= split && second is not null && !ReferenceEquals(target, second)) target = second;
var item = menu.Items[i];
var grid = menu.GridOf(i);
// Grupo novo: título (ou só o fio). Menu comum: logo abaixo da grade do topo sempre há o fio separando as duas partes.
var heading = menu.HasSectionGrids || !hasGrid
? (i == 0 ? item.Section is not null : item.Section != section)
: i != 0 && (i == menu.QuickCount || item.Section != section);
if (heading) stack.Children.Add(SectionHeading(titles || (item.Section is not null && item.Section == menu.TitledSection) ? item.Section : null, first: i == 0));
if (heading) target.Children.Add(SectionHeading(titles || (item.Section is not null && item.Section == menu.TitledSection) ? item.Section : null, first: target.Children.Count == 0));
section = item.Section;
if (grid is null)
{
positions[i] = stack.Children.Count;
stack.Children.Add(MenuRow(app, menu, i));
positions[i] = target.Children.Count;
owners[i] = target;
target.Children.Add(MenuRow(app, menu, i));
i++;
continue;
}
var tiles = new Grid { ColumnSpacing = Theme.SpaceS, RowSpacing = Theme.SpaceS, Margin = new Thickness(0, menu.HasSectionGrids ? Theme.Space(4) : 0, 0, menu.HasSectionGrids ? Theme.Space(4) : 0) };
for (var c = 0; c < grid.Columns; c++) tiles.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
for (var r = 0; r < grid.Rows; r++) tiles.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
for (var t = grid.Start; t < grid.End; t++) tiles.Children.Add(MenuTile(app, menu, t));
stack.Children.Add(tiles);
target.Children.Add(tiles);
grids[grid] = tiles;
i = grid.End;
}
UIElement body = stack;
if (second is not null)
{
var columns = new Grid { ColumnSpacing = Theme.SpaceL };
columns.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
columns.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Grid.SetColumn(second, 1);
columns.Children.Add(stack);
columns.Children.Add(second);
body = columns;
}
if (menu.Items.Count == 0)
stack.Children.Add(new TextBlock { Text = "Nenhuma opção.", FontSize = Theme.FontBody, Foreground = Theme.TextMuted });

Expand All @@ -661,13 +685,13 @@ private static Border BuildMenu(AppController app, MenuModal menu, out Action up
description.Show(menu.Description);

Border? footer = null;
var card = Panel(app, Header(menu, compact: true), stack, menu.Size, footerSink: f => footer = f, compact: true, below: below);
var card = Panel(app, Header(menu, compact: true), body, menu.Size, footerSink: f => footer = f, compact: true, below: below);
var shown = menu.FocusIndex;
void Replace(int index)
{
if (index < 0 || index >= positions.Length) return;
if (menu.GridOf(index) is { } grid) grids[grid].Children[index - grid.Start] = MenuTile(app, menu, index);
else stack.Children[positions[index]] = MenuRow(app, menu, index);
else owners[index].Children[positions[index]] = MenuRow(app, menu, index);
}
update = () =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/ControlFS.Application/AppController.Browser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,14 +575,14 @@ private void ShowAppMenu()
const MenuPlacement quick = MenuPlacement.Quick;
List<MenuItem> items =
[
new("Configurações…", ShowSettings, Detail: "Tema, exibição, sons, controles, privacidade e atualizações. Sempre o primeiro bloco do Menu.", Icon: ActionIcon.Settings, Placement: quick),
new(PasteLabel, () => Paste(pane), inBrowser ? PasteUnavailable(pane) : "Abra uma pasta do disco para colar.", Icon: ActionIcon.Paste, Placement: quick, ShortLabel: "Colar"),
new("Nova pasta", () => BeginCreateFolder(pane), inBrowser && pane.Location is PhysicalLocation ? null : "Abra uma pasta do disco primeiro.", Icon: ActionIcon.NewFolder, Placement: quick),
new("Nova aba", NewTabHere, NewTabUnavailable, Detail: "Abre a pasta atual numa aba nova. Com 2+ abas, L2/R2 trocam de aba.", Icon: ActionIcon.NewTab, Placement: quick),
new("Atualizar", () => Refresh(pane), inBrowser ? null : "Nada para atualizar na tela inicial.", Icon: ActionIcon.Refresh, Placement: quick),
new("Ir para caminho…", () => BeginGoToPath(pane), Detail: "Digite ou cole o caminho de uma pasta (ex.: D:\\Jogos).", Icon: ActionIcon.GoToPath, Placement: quick, ShortLabel: "Caminho"),
new($"Operações ({Plural.Of(Operations.ActiveCount, "ativa", "ativas")})", ShowOperations, Operations.Items.Count == 0 && History.Entries.Count == 0 ? "Nenhuma operação registrada." : null,
Icon: ActionIcon.Operations, Placement: quick, ShortLabel: "Operações"),
new("Configurações…", ShowSettings, Detail: "Exibição, busca, privacidade, controles e atualizações.", Icon: ActionIcon.Settings, Placement: quick),
new("Ir para o início", GoHome, Screen == Screen.Home ? "Você já está no início." : null, Icon: ActionIcon.Home, Placement: quick, ShortLabel: "Início"),
FullScreenTile(),
new("Ir para pasta acima…", () => ShowPathMenu(pane), inBrowser && BuildBreadcrumbs(pane).Count > 1 ? null : "Não há pastas acima desta.",
Expand Down
6 changes: 6 additions & 0 deletions src/ControlFS.Application/AppController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,12 @@ private void HandleMenu(MenuModal menu, InputAction action)
break;
case InputAction.PageUp: menu.FocusIndex = 0; break;
case InputAction.PageDown: menu.FocusIndex = count - 1; break;
case InputAction.NextRegion or InputAction.PreviousRegion when menu.SectionStarts is { Count: > 1 } starts:
// Configurações (#285): R1/L1 pulam para o primeiro ajuste do grupo seguinte/anterior (dão a volta).
var current = starts.ToList().FindLastIndex(s => s <= menu.FocusIndex);
var step = action == InputAction.NextRegion ? 1 : -1;
menu.FocusIndex = starts[((current + step) % starts.Count + starts.Count) % starts.Count];
break;
case InputAction.Confirm:
case InputAction.NavigateRight when !menu.IsQuick(menu.FocusIndex):
ChooseMenuItem(menu);
Expand Down
57 changes: 56 additions & 1 deletion src/ControlFS.Application/State/Modals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public enum ModalSize
/// <summary>Teclado virtual, "Mais da equipe", assistente e teste de controles.</summary>
Wide,

/// <summary>
/// Configurações (#285): paisagem. Os grupos ficam em duas colunas lado a lado quando a janela comporta (1280×720 e
/// maiores); numa janela estreita voltam a uma coluna só, como o tamanho Médio.
/// </summary>
Landscape,

/// <summary>A janela menos as margens (visualizações, boas-vindas).</summary>
Fill,
}
Expand Down Expand Up @@ -140,6 +146,55 @@ private int InitialFocus(MenuItem first)
public IReadOnlyList<MenuItem> Items { get; private set; } = [];
public int FocusIndex { get; internal set; }

/// <summary>
/// Onde começa cada grupo de Configurações (índice do primeiro item), na ordem de <see cref="Items"/>: R1/L1 pulam de um
/// grupo para outro (#285). Vazio fora de menus com grupos.
/// </summary>
public IReadOnlyList<int> SectionStarts
{
get
{
if (!_sectionGrids) return [];
var starts = new List<int>();
for (var i = 0; i < Items.Count; i++)
if (i == 0 || Items[i].Section != Items[i - 1].Section) starts.Add(i);
return starts;
}
}

/// <summary>
/// Índice do primeiro item da coluna da direita quando os grupos ficam lado a lado (#285): o ponto de corte entre grupos que
/// deixa as duas colunas com alturas parecidas (tiles pesam mais que linhas). A ordem de navegação segue a de leitura em colunas:
/// Baixo no fim da primeira coluna vai ao topo da segunda.
/// </summary>
public int ColumnSplit
{
get
{
var starts = SectionStarts;
if (starts.Count < 2) return Items.Count;
double Weight(int from, int to)
{
double w = 1.2; // título do grupo
for (var i = from; i < to;)
{
if (GridOf(i) is { } grid) { w += grid.Rows * 2.0; i = grid.End; }
else { w += 1; i++; }
}
return w;
}
var total = Weight(0, Items.Count);
var best = starts[1];
var bestGap = double.MaxValue;
foreach (var cut in starts.Skip(1))
{
var gap = Math.Abs(Weight(0, cut) - (total - Weight(0, cut)));
if (gap < bestGap) (best, bestGap) = (cut, gap);
}
return best;
}
}

/// <summary>As grades do menu, na ordem de <see cref="Items"/> (menu comum: no máximo uma, no topo).</summary>
public IReadOnlyList<MenuGrid> Grids { get; private set; } = [];

Expand Down Expand Up @@ -200,7 +255,7 @@ public string DescriptionOf(int index)
public IEnumerable<string> AllDescriptions => Enumerable.Range(0, Items.Count).Select(DescriptionOf).Where(t => t.Length > 0).Distinct();

/// <summary>Menu com grade ou seletor de opções: <see cref="ModalSize.Medium"/>; só lista: <see cref="ModalSize.Compact"/>. Nunca pelo item em foco (#227).</summary>
public override ModalSize Size => Grids.Count > 0 || IsPicker ? ModalSize.Medium : ModalSize.Compact;
public override ModalSize Size => _sectionGrids ? ModalSize.Landscape : Grids.Count > 0 || IsPicker ? ModalSize.Medium : ModalSize.Compact;

/// <summary>
/// Seletor de opções (#261): cada item é uma alternativa de um valor; <see cref="PickerCurrent"/> é a atual (marcada com
Expand Down
4 changes: 2 additions & 2 deletions tests/ControlFS.UnitTests/Application/HintJourneyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ public void Dialogs_and_menus_name_the_focused_choice_and_hide_confirm_on_unavai
app.Start();
var d = new Driver(app);

d.Press(InputAction.OpenAppMenu); // na tela inicial, o primeiro item (Colar) está indisponível
d.Press(InputAction.OpenAppMenu); // na tela inicial, Colar (o segundo bloco; Configurações é o primeiro) está indisponível
var menu = await d.WaitMenu();
Assert.True(menu.Items[menu.FocusIndex].IsEnabled); // o menu nunca abre numa opção indisponível
d.Press(InputAction.PageUp); // leva o foco até Colar
d.Press(InputAction.NavigateRight); // leva o foco até Colar
Assert.False(menu.Items[menu.FocusIndex].IsEnabled);
Assert.Null(Label(app, InputAction.Confirm));
while (!menu.Items[menu.FocusIndex].IsEnabled) d.Press(InputAction.NavigateDown);
Expand Down
Loading
Loading