Skip to content
Open
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
25 changes: 25 additions & 0 deletions VintageStoryModManager/Converters/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,28 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
throw new NotSupportedException();
}
}

/// <summary>
/// Extracts the category name from a CategoryGroupKey.
/// The CategoryGroupKey format is "{Order:D10}|{CategoryName}".
/// </summary>
public class CategoryNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not string groupKey || string.IsNullOrEmpty(groupKey))
return string.Empty;

// The format is "{Order:D10}|{CategoryName}"
var pipeIndex = groupKey.IndexOf('|');
if (pipeIndex >= 0 && pipeIndex < groupKey.Length - 1)
return groupKey.Substring(pipeIndex + 1);

return groupKey;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
103 changes: 103 additions & 0 deletions VintageStoryModManager/Models/ModCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace VintageStoryModManager.Models;

/// <summary>
/// Represents a category for organizing mods in the mod list.
/// </summary>
public sealed class ModCategory
{
/// <summary>
/// The ID used for the default "Uncategorized" category.
/// </summary>
public const string UncategorizedId = "uncategorized";

/// <summary>
/// The display name for the default "Uncategorized" category.
/// </summary>
public const string UncategorizedName = "Uncategorized";

/// <summary>
/// Creates a new category with a unique ID.
/// </summary>
public ModCategory(string name, int order = 0)
{
Id = Guid.NewGuid().ToString("N");
Name = name ?? throw new ArgumentNullException(nameof(name));
Order = order;
}

/// <summary>
/// Creates a category with a specific ID (used for deserialization or default category).
/// </summary>
public ModCategory(string id, string name, int order, bool isProfileSpecific = false)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = name ?? throw new ArgumentNullException(nameof(name));
Order = order;
IsProfileSpecific = isProfileSpecific;
}

/// <summary>
/// Parameterless constructor for JSON deserialization.
/// </summary>
public ModCategory()
{
Id = string.Empty;
Name = string.Empty;
}

/// <summary>
/// Unique identifier for this category.
/// </summary>
public string Id { get; set; }

/// <summary>
/// Display name of the category.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Sort order for the category. Lower values appear first.
/// </summary>
public int Order { get; set; }

/// <summary>
/// Whether this is the default "Uncategorized" category that cannot be deleted.
/// </summary>
public bool IsDefault => string.Equals(Id, UncategorizedId, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Whether this category is specific to the current profile (vs global).
/// </summary>
public bool IsProfileSpecific { get; set; }

/// <summary>
/// Creates the default "Uncategorized" category.
/// </summary>
public static ModCategory CreateDefault()
{
return new ModCategory(UncategorizedId, UncategorizedName, int.MaxValue);
}

/// <summary>
/// Creates a copy of this category.
/// </summary>
public ModCategory Clone()
{
return new ModCategory(Id, Name, Order, IsProfileSpecific);
}

public override string ToString()
{
return $"{Name} ({Id})";
}

public override bool Equals(object? obj)
{
return obj is ModCategory other && string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}

public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(Id);
}
}
41 changes: 22 additions & 19 deletions VintageStoryModManager/Resources/MainWindowStyles.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<converters:BooleanToVisibilityConverter x:Key="IMM.VisibleWhenTrueConverter" />
<converters:BooleanToVisibilityConverter x:Key="IMM.VisibleWhenFalseConverter" IsInverted="True" />
<converters:BooleanToVisibilityConverter x:Key="IMM.HiddenWhenFalseConverter" UseHidden="True" />
<converters:CategoryNameConverter x:Key="IMM.CategoryNameConverter" />
<Thickness x:Key="IMM.RowPaddingBase">0,0,0,0</Thickness>
<sys:Double x:Key="IMM.NormalRowHeightOffset">10</sys:Double>
<sys:Double x:Key="IMM.CompactRowHeightOffset">-12</sys:Double>
Expand Down Expand Up @@ -381,31 +382,33 @@

<Popup
x:Name="SubMenuPopup"
Margin="0,12,0,12"
AllowsTransparency="True"
Focusable="False"
HorizontalOffset="2"
HorizontalOffset="-10"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Right"
PlacementTarget="{Binding ElementName=templateRoot}"
VerticalOffset="-4">

<Border
MinWidth="{Binding ActualWidth, ElementName=templateRoot}"
Margin="0,0,0,4"
Background="{DynamicResource Brush.Menu.Item.Background.Hover}"
BorderBrush="{DynamicResource Brush.Menu.Border}"
BorderThickness="1,1,1,1"
CornerRadius="4"
SnapsToDevicePixels="True">
<ScrollViewer
CanContentScroll="False"
Focusable="False"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Auto">
<ItemsPresenter Margin="4" />
</ScrollViewer>
</Border>
<Grid>
<Border Background="#01000000" />
<Border
MinWidth="{Binding ActualWidth, ElementName=templateRoot}"
Margin="12,0,0,0"
Background="{DynamicResource Brush.Menu.Item.Background.Hover}"
BorderBrush="{DynamicResource Brush.Menu.Border}"
BorderThickness="1,1,1,1"
CornerRadius="4"
SnapsToDevicePixels="True">
<ScrollViewer
CanContentScroll="False"
Focusable="False"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Auto">
<ItemsPresenter Margin="4" />
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>

Expand All @@ -429,7 +432,7 @@
<Trigger Property="Role" Value="TopLevelHeader">
<Setter TargetName="SubMenuPopup" Property="Placement" Value="Bottom" />
<Setter TargetName="SubMenuPopup" Property="HorizontalOffset" Value="0" />
<Setter TargetName="SubMenuPopup" Property="VerticalOffset" Value="0" />
<Setter TargetName="SubMenuPopup" Property="VerticalOffset" Value="-8" />
<Setter TargetName="ArrowGlyph" Property="Visibility" Value="Collapsed" />
</Trigger>

Expand Down
57 changes: 32 additions & 25 deletions VintageStoryModManager/Resources/Themes/DarkVsTheme.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -507,17 +507,20 @@
HorizontalOffset="0"
IsOpen="{TemplateBinding IsSubmenuOpen}"
Placement="Bottom"
VerticalOffset="20">
<Border
x:Name="SubMenuBorder"
Background="{DynamicResource Brush.Menu.Submenu.Background}"
BorderBrush="{DynamicResource Brush.Menu.Submenu.Border}"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer CanContentScroll="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
</Border>
VerticalOffset="-8">
<Grid Background="Transparent">
<Border
x:Name="SubMenuBorder"
Margin="0,10,0,0"
Background="{DynamicResource Brush.Menu.Submenu.Background}"
BorderBrush="{DynamicResource Brush.Menu.Submenu.Border}"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer CanContentScroll="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
Expand Down Expand Up @@ -599,24 +602,28 @@
x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
HorizontalOffset="2"
HorizontalOffset="-10"
IsOpen="{TemplateBinding IsSubmenuOpen}"
Placement="Right"
PopupAnimation="Fade"
VerticalOffset="-3">
<Border
x:Name="SubMenuBorder"
Background="{DynamicResource Brush.Menu.Submenu.Background}"
BorderBrush="{DynamicResource Brush.Menu.Submenu.Border}"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer CanContentScroll="True">
<StackPanel
Margin="0"
IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
</Border>
<Grid>
<Border Background="#01000000" />
<Border
x:Name="SubMenuBorder"
Margin="12,0,0,0"
Background="{DynamicResource Brush.Menu.Submenu.Background}"
BorderBrush="{DynamicResource Brush.Menu.Submenu.Border}"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer CanContentScroll="True">
<StackPanel
Margin="0"
IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
Expand Down
Loading