diff --git a/main.go b/main.go index 64927a8..a4e2d4e 100644 --- a/main.go +++ b/main.go @@ -18,10 +18,10 @@ import ( const appVersion = "1.1.2" func main() { - myApp := app.NewWithID("com.tairasu.turtlesilicon") - myWindow := myApp.NewWindow("TurtleSilicon v" + appVersion) - myWindow.Resize(fyne.NewSize(650, 500)) - myWindow.SetFixedSize(true) + TSApp := app.NewWithID("com.tairasu.turtlesilicon") + TSWindow := TSApp.NewWindow("TurtleSilicon v" + appVersion) + TSWindow.Resize(fyne.NewSize(650, 500)) + TSWindow.SetFixedSize(true) // Check for updates go func() { @@ -47,19 +47,19 @@ func main() { prefs.SuppressedUpdateVersion = latestVersion utils.SavePrefs(prefs) } - }, myWindow) + }, TSWindow) } }() - content := ui.CreateUI(myWindow) - myWindow.SetContent(content) + content := ui.CreateUI(TSWindow) + TSWindow.SetContent(content) // Set up cleanup when window closes - myWindow.SetCloseIntercept(func() { + TSWindow.SetCloseIntercept(func() { log.Println("Application closing, cleaning up RosettaX87 service...") service.CleanupService() - myApp.Quit() + TSApp.Quit() }) - myWindow.ShowAndRun() + TSWindow.ShowAndRun() } diff --git a/pkg/ui/components.go b/pkg/ui/components.go new file mode 100644 index 0000000..d3f0a8f --- /dev/null +++ b/pkg/ui/components.go @@ -0,0 +1,160 @@ +package ui + +import ( + "log" + "net/url" + + "turtlesilicon/pkg/launcher" + "turtlesilicon/pkg/patching" + "turtlesilicon/pkg/service" + "turtlesilicon/pkg/utils" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +// createOptionsComponents initializes all option-related UI components +func createOptionsComponents() { + // Load preferences for initial values + prefs, _ := utils.LoadPrefs() + + metalHudCheckbox = widget.NewCheck("Enable Metal Hud (show FPS)", func(checked bool) { + launcher.EnableMetalHud = checked + log.Printf("Metal HUD enabled: %v", launcher.EnableMetalHud) + }) + metalHudCheckbox.SetChecked(launcher.EnableMetalHud) + + showTerminalCheckbox = widget.NewCheck("Show Terminal", func(checked bool) { + // Save to preferences + prefs, _ := utils.LoadPrefs() + prefs.ShowTerminalNormally = checked + utils.SavePrefs(prefs) + log.Printf("Show terminal normally: %v", checked) + }) + showTerminalCheckbox.SetChecked(prefs.ShowTerminalNormally) + + vanillaTweaksCheckbox = widget.NewCheck("Enable vanilla-tweaks", func(checked bool) { + launcher.EnableVanillaTweaks = checked + // Save to preferences + prefs, _ := utils.LoadPrefs() + prefs.EnableVanillaTweaks = checked + utils.SavePrefs(prefs) + log.Printf("Vanilla-tweaks enabled: %v", launcher.EnableVanillaTweaks) + }) + vanillaTweaksCheckbox.SetChecked(prefs.EnableVanillaTweaks) + launcher.EnableVanillaTweaks = prefs.EnableVanillaTweaks + + // Load environment variables from preferences + if prefs.EnvironmentVariables != "" { + launcher.CustomEnvVars = prefs.EnvironmentVariables + } + + envVarsEntry = widget.NewEntry() + envVarsEntry.SetPlaceHolder(`Custom environment variables (KEY=VALUE format)`) + envVarsEntry.SetText(launcher.CustomEnvVars) + envVarsEntry.OnChanged = func(text string) { + launcher.CustomEnvVars = text + // Save to preferences + prefs, _ := utils.LoadPrefs() + prefs.EnvironmentVariables = text + utils.SavePrefs(prefs) + log.Printf("Environment variables updated: %v", launcher.CustomEnvVars) + } +} + +// createPatchingButtons creates all patching-related buttons +func createPatchingButtons(myWindow fyne.Window) { + patchTurtleWoWButton = widget.NewButton("Patch TurtleWoW", func() { + patching.PatchTurtleWoW(myWindow, UpdateAllStatuses) + }) + unpatchTurtleWoWButton = widget.NewButton("Unpatch TurtleWoW", func() { + patching.UnpatchTurtleWoW(myWindow, UpdateAllStatuses) + }) + patchCrossOverButton = widget.NewButton("Patch CrossOver", func() { + patching.PatchCrossOver(myWindow, UpdateAllStatuses) + }) + unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() { + patching.UnpatchCrossOver(myWindow, UpdateAllStatuses) + }) +} + +// createServiceButtons creates service-related buttons +func createServiceButtons(myWindow fyne.Window) { + startServiceButton = widget.NewButton("Start Service", func() { + service.StartRosettaX87Service(myWindow, UpdateAllStatuses) + }) + stopServiceButton = widget.NewButton("Stop Service", func() { + service.StopRosettaX87Service(myWindow, UpdateAllStatuses) + }) +} + +// createLaunchButton creates the legacy launch button +func createLaunchButton(myWindow fyne.Window) { + launchButton = widget.NewButton("Launch Game", func() { + launcher.LaunchGame(myWindow) + }) +} + +// createBottomBar creates the bottom bar with Options, GitHub, and PLAY buttons +func createBottomBar(myWindow fyne.Window) fyne.CanvasObject { + // Set the current window for popup functionality + currentWindow = myWindow + + // Options button + optionsButton := widget.NewButton("Options", func() { + showOptionsPopup() + }) + + // GitHub button + githubButton := widget.NewButton("GitHub", func() { + githubURL := "https://github.com/tairasu/TurtleSilicon" + parsedURL, err := url.Parse(githubURL) + if err != nil { + log.Printf("Error parsing GitHub URL: %v", err) + return + } + fyne.CurrentApp().OpenURL(parsedURL) + }) + playButtonText = widget.NewRichTextFromMarkdown("# PLAY") + playButtonText.Wrapping = fyne.TextWrapOff + + playButton = widget.NewButton("", func() { + launcher.LaunchGame(myWindow) + }) + playButton.Importance = widget.HighImportance + playButton.Disable() + + playButtonWithText := container.NewStack( + playButton, + container.NewCenter(playButtonText), + ) + + leftButtons := container.NewHBox( + optionsButton, + widget.NewSeparator(), // Visual separator + githubButton, + ) + + // Create the large play button with fixed size + buttonWidth := float32(120) + buttonHeight := float32(80) + playButtonWithText.Resize(fyne.NewSize(buttonWidth, buttonHeight)) + + // Create a container for the play button that ensures it's positioned at bottom-right + playButtonContainer := container.NewWithoutLayout(playButtonWithText) + playButtonContainer.Resize(fyne.NewSize(buttonWidth+40, buttonHeight+20)) // Add padding + + playButtonWithText.Move(fyne.NewPos(-50, -32)) + + // Use border layout to position elements + bottomContainer := container.NewBorder( + nil, // top + nil, // bottom + leftButtons, // left + playButtonContainer, // right - our large play button + nil, // center + ) + + return container.NewPadded(bottomContainer) +} diff --git a/pkg/ui/layout.go b/pkg/ui/layout.go new file mode 100644 index 0000000..3055710 --- /dev/null +++ b/pkg/ui/layout.go @@ -0,0 +1,108 @@ +package ui + +import ( + "log" + + "turtlesilicon/pkg/paths" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +// createHeaderContainer creates the header with title and subtitle +func createHeaderContainer() fyne.CanvasObject { + // Main title + titleText := widget.NewRichTextFromMarkdown("# TurtleSilicon") + titleText.Wrapping = fyne.TextWrapOff + + // Subtitle + subtitleText := widget.NewLabel("A TurtleWoW launcher for Apple Silicon Macs") + subtitleText.Alignment = fyne.TextAlignCenter + + // Create header container + headerContainer := container.NewVBox( + container.NewCenter(titleText), + container.NewCenter(subtitleText), + ) + + return headerContainer +} + +// createLogoContainer creates and returns the application logo container +func createLogoContainer() fyne.CanvasObject { + // Load the application logo + logoResource, err := fyne.LoadResourceFromPath("Icon.png") + if err != nil { + log.Printf("Warning: could not load logo: %v", err) + } + + // Create the logo image with a smaller fixed size since we have a header now + var logoImage *canvas.Image + if logoResource != nil { + logoImage = canvas.NewImageFromResource(logoResource) + logoImage.FillMode = canvas.ImageFillContain + logoImage.SetMinSize(fyne.NewSize(80, 80)) + } + + // Create a container to center the logo + var logoContainer fyne.CanvasObject + if logoImage != nil { + logoContainer = container.NewCenter(logoImage) + } else { + // If logo couldn't be loaded, add an empty space for consistent layout + logoContainer = container.NewCenter(widget.NewLabel("")) + } + + return logoContainer +} + +// createPathSelectionForm creates the form for selecting CrossOver and TurtleWoW paths +func createPathSelectionForm(myWindow fyne.Window) *widget.Form { + pathSelectionForm := widget.NewForm( + widget.NewFormItem("CrossOver Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { + paths.SelectCrossOverPath(myWindow, crossoverPathLabel, UpdateAllStatuses) + }), crossoverPathLabel)), + widget.NewFormItem("TurtleWoW Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { + paths.SelectTurtleWoWPath(myWindow, turtlewowPathLabel, UpdateAllStatuses) + }), turtlewowPathLabel)), + ) + + return pathSelectionForm +} + +// createPatchOperationsLayout creates the layout for patch operations +func createPatchOperationsLayout() fyne.CanvasObject { + patchOperationsLayout := container.NewVBox( + widget.NewSeparator(), + container.NewGridWithColumns(4, + widget.NewLabel("TurtleWoW Patch:"), turtlewowStatusLabel, patchTurtleWoWButton, unpatchTurtleWoWButton, + ), + container.NewGridWithColumns(4, + widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, unpatchCrossOverButton, + ), + container.NewGridWithColumns(4, + widget.NewLabel("RosettaX87 Service:"), serviceStatusLabel, startServiceButton, stopServiceButton, + ), + widget.NewSeparator(), + ) + + return patchOperationsLayout +} + +// createMainContent creates the main content area of the application +func createMainContent(myWindow fyne.Window) fyne.CanvasObject { + logoContainer := createLogoContainer() + pathSelectionForm := createPathSelectionForm(myWindow) + patchOperationsLayout := createPatchOperationsLayout() + + // Create main content area with better spacing + mainContent := container.NewVBox( + logoContainer, + pathSelectionForm, + patchOperationsLayout, + ) + + return mainContent +} diff --git a/pkg/ui/popup.go b/pkg/ui/popup.go new file mode 100644 index 0000000..18f7e2e --- /dev/null +++ b/pkg/ui/popup.go @@ -0,0 +1,71 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +// showOptionsPopup creates and shows an integrated popup window for options +func showOptionsPopup() { + if currentWindow == nil { + return + } + + // Create options content with better organization and smaller titles + optionsTitle := widget.NewLabel("Options") + optionsTitle.TextStyle = fyne.TextStyle{Bold: true} + gameOptionsContainer := container.NewVBox( + optionsTitle, + widget.NewSeparator(), + metalHudCheckbox, + showTerminalCheckbox, + vanillaTweaksCheckbox, + ) + + envVarsTitle := widget.NewLabel("Environment Variables") + envVarsTitle.TextStyle = fyne.TextStyle{Bold: true} + envVarsContainer := container.NewVBox( + envVarsTitle, + widget.NewSeparator(), + envVarsEntry, + ) + + // Create a scrollable container for all options + optionsContent := container.NewVBox( + gameOptionsContainer, + envVarsContainer, + ) + + scrollContainer := container.NewScroll(optionsContent) + + // Create close button + closeButton := widget.NewButton("Close", func() { + // This will be set when the popup is created + }) + + // Create the popup content with close button + popupContent := container.NewBorder( + nil, // top + container.NewCenter(closeButton), // bottom + nil, // left + nil, // right + container.NewPadded(scrollContainer), // center + ) + + // Get the window size and calculate 2/3 size + windowSize := currentWindow.Content().Size() + popupWidth := windowSize.Width * 2 / 3 + popupHeight := windowSize.Height * 2 / 3 + + // Create a modal popup + popup := widget.NewModalPopUp(popupContent, currentWindow.Canvas()) + popup.Resize(fyne.NewSize(popupWidth, popupHeight)) + + // Set the close button action to hide the popup + closeButton.OnTapped = func() { + popup.Hide() + } + + popup.Show() +} diff --git a/pkg/ui/status.go b/pkg/ui/status.go new file mode 100644 index 0000000..5ebc44e --- /dev/null +++ b/pkg/ui/status.go @@ -0,0 +1,245 @@ +package ui + +import ( + "os" + "path/filepath" + "strings" + "time" + + "turtlesilicon/pkg/paths" + "turtlesilicon/pkg/service" + "turtlesilicon/pkg/utils" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var ( + pulsingActive = false +) + +// UpdateAllStatuses updates all UI components based on current application state +func UpdateAllStatuses() { + updateCrossoverStatus() + updateTurtleWoWStatus() + updatePlayButtonState() + updateServiceStatus() +} + +// updateCrossoverStatus updates CrossOver path and patch status +func updateCrossoverStatus() { + if paths.CrossoverPath == "" { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + paths.PatchesAppliedCrossOver = false // Reset if path is cleared + } else { + crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.CrossoverPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + wineloader2Path := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") + if utils.PathExists(wineloader2Path) { + paths.PatchesAppliedCrossOver = true + } + } + crossoverPathLabel.Refresh() + + if paths.PatchesAppliedCrossOver { + crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + if patchCrossOverButton != nil { + patchCrossOverButton.Disable() + } + if unpatchCrossOverButton != nil { + unpatchCrossOverButton.Enable() + } + } else { + crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + if patchCrossOverButton != nil { + if paths.CrossoverPath != "" { + patchCrossOverButton.Enable() + } else { + patchCrossOverButton.Disable() + } + } + if unpatchCrossOverButton != nil { + unpatchCrossOverButton.Disable() + } + } + crossoverStatusLabel.Refresh() +} + +// updateTurtleWoWStatus updates TurtleWoW path and patch status +func updateTurtleWoWStatus() { + if paths.TurtlewowPath == "" { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + paths.PatchesAppliedTurtleWoW = false // Reset if path is cleared + } else { + turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.TurtlewowPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + + // Check if all required files exist + winerosettaDllPath := filepath.Join(paths.TurtlewowPath, "winerosetta.dll") + d3d9DllPath := filepath.Join(paths.TurtlewowPath, "d3d9.dll") + libSiliconPatchDllPath := filepath.Join(paths.TurtlewowPath, "libSiliconPatch.dll") + rosettaX87DirPath := filepath.Join(paths.TurtlewowPath, "rosettax87") + dllsTextFile := filepath.Join(paths.TurtlewowPath, "dlls.txt") + rosettaX87ExePath := filepath.Join(rosettaX87DirPath, "rosettax87") + libRuntimeRosettaX87Path := filepath.Join(rosettaX87DirPath, "libRuntimeRosettax87") + + dllsFileValid := false + if utils.PathExists(dllsTextFile) { + if fileContent, err := os.ReadFile(dllsTextFile); err == nil { + contentStr := string(fileContent) + if strings.Contains(contentStr, "winerosetta.dll") && strings.Contains(contentStr, "libSiliconPatch.dll") { + dllsFileValid = true + } + } + } + + if utils.PathExists(winerosettaDllPath) && utils.PathExists(d3d9DllPath) && utils.PathExists(libSiliconPatchDllPath) && + utils.DirExists(rosettaX87DirPath) && utils.PathExists(rosettaX87ExePath) && + utils.PathExists(libRuntimeRosettaX87Path) && dllsFileValid { + paths.PatchesAppliedTurtleWoW = true + } + } + turtlewowPathLabel.Refresh() + + if paths.PatchesAppliedTurtleWoW { + turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + if patchTurtleWoWButton != nil { + patchTurtleWoWButton.Disable() + } + if unpatchTurtleWoWButton != nil { + unpatchTurtleWoWButton.Enable() + } + } else { + turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + if patchTurtleWoWButton != nil { + if paths.TurtlewowPath != "" { + patchTurtleWoWButton.Enable() + } else { + patchTurtleWoWButton.Disable() + } + } + if unpatchTurtleWoWButton != nil { + unpatchTurtleWoWButton.Disable() + } + } + turtlewowStatusLabel.Refresh() +} + +// updatePlayButtonState enables/disables play and launch buttons based on current state +func updatePlayButtonState() { + launchEnabled := paths.PatchesAppliedTurtleWoW && paths.PatchesAppliedCrossOver && + paths.TurtlewowPath != "" && paths.CrossoverPath != "" && service.IsServiceRunning() + + if launchButton != nil { + if launchEnabled { + launchButton.Enable() + } else { + launchButton.Disable() + } + } + + if playButton != nil && playButtonText != nil { + if launchEnabled { + playButton.Enable() + // Update text to show enabled state with bright color + playButtonText.Segments = []widget.RichTextSegment{ + &widget.TextSegment{ + Text: "PLAY", + Style: widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + ColorName: theme.ColorNameForeground, + }, + }, + } + } else { + playButton.Disable() + // Update text to show disabled state with dimmed color and different text + playButtonText.Segments = []widget.RichTextSegment{ + &widget.TextSegment{ + Text: "PLAY", + Style: widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + ColorName: theme.ColorNameDisabled, + }, + }, + } + } + playButtonText.Refresh() + } +} + +// updateServiceStatus updates RosettaX87 service status and related buttons +func updateServiceStatus() { + if paths.ServiceStarting { + // Show pulsing "Starting..." when service is starting + if serviceStatusLabel != nil { + if !pulsingActive { + pulsingActive = true + go startPulsingAnimation() + } + } + if startServiceButton != nil { + startServiceButton.Disable() + } + if stopServiceButton != nil { + stopServiceButton.Disable() + } + } else if service.IsServiceRunning() { + pulsingActive = false + paths.RosettaX87ServiceRunning = true + if serviceStatusLabel != nil { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Running", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + serviceStatusLabel.Refresh() + } + if startServiceButton != nil { + startServiceButton.Disable() + } + if stopServiceButton != nil { + stopServiceButton.Enable() + } + } else { + pulsingActive = false + paths.RosettaX87ServiceRunning = false + if serviceStatusLabel != nil { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Stopped", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + serviceStatusLabel.Refresh() + } + if startServiceButton != nil { + if paths.TurtlewowPath != "" && paths.PatchesAppliedTurtleWoW { + startServiceButton.Enable() + } else { + startServiceButton.Disable() + } + } + if stopServiceButton != nil { + stopServiceButton.Disable() + } + } +} + +// startPulsingAnimation creates a pulsing effect for the "Starting..." text +func startPulsingAnimation() { + dots := 0 + for pulsingActive && paths.ServiceStarting { + var text string + switch dots % 4 { + case 0: + text = "Starting" + case 1: + text = "Starting." + case 2: + text = "Starting.." + case 3: + text = "Starting..." + } + + if serviceStatusLabel != nil { + fyne.DoAndWait(func() { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: text, Style: widget.RichTextStyle{ColorName: theme.ColorNamePrimary}}} + serviceStatusLabel.Refresh() + }) + } + + time.Sleep(500 * time.Millisecond) + dots++ + } +} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 2eac8c9..3d316fe 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -1,230 +1,22 @@ package ui import ( - "log" - "net/url" - "os" // Added import for os.ReadFile - "path/filepath" - "strings" - "time" - - "turtlesilicon/pkg/launcher" // Corrected import path - "turtlesilicon/pkg/patching" // Corrected import path - "turtlesilicon/pkg/paths" // Corrected import path - "turtlesilicon/pkg/service" // Added service import - "turtlesilicon/pkg/utils" // Corrected import path + "turtlesilicon/pkg/paths" + "turtlesilicon/pkg/utils" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) -var ( - crossoverPathLabel *widget.RichText - turtlewowPathLabel *widget.RichText - turtlewowStatusLabel *widget.RichText - crossoverStatusLabel *widget.RichText - serviceStatusLabel *widget.RichText - launchButton *widget.Button - patchTurtleWoWButton *widget.Button - patchCrossOverButton *widget.Button - unpatchTurtleWoWButton *widget.Button - unpatchCrossOverButton *widget.Button - startServiceButton *widget.Button - stopServiceButton *widget.Button - metalHudCheckbox *widget.Check - showTerminalCheckbox *widget.Check - vanillaTweaksCheckbox *widget.Check - envVarsEntry *widget.Entry - pulsingActive = false -) - -func UpdateAllStatuses() { - // Update Crossover Path and Status - if paths.CrossoverPath == "" { - crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - paths.PatchesAppliedCrossOver = false // Reset if path is cleared - } else { - crossoverPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.CrossoverPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - wineloader2Path := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") - if utils.PathExists(wineloader2Path) { - paths.PatchesAppliedCrossOver = true - } else { - // paths.PatchesAppliedCrossOver = false // Only set to false if not already true from a patch action this session - } - } - crossoverPathLabel.Refresh() - - if paths.PatchesAppliedCrossOver { - crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - if patchCrossOverButton != nil { - patchCrossOverButton.Disable() - } - if unpatchCrossOverButton != nil { - unpatchCrossOverButton.Enable() - } - } else { - crossoverStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - if patchCrossOverButton != nil { - if paths.CrossoverPath != "" { - patchCrossOverButton.Enable() - } else { - patchCrossOverButton.Disable() - } - } - if unpatchCrossOverButton != nil { - unpatchCrossOverButton.Disable() - } - } - crossoverStatusLabel.Refresh() - - // Update TurtleWoW Path and Status - if paths.TurtlewowPath == "" { - turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not set", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - paths.PatchesAppliedTurtleWoW = false // Reset if path is cleared - } else { - turtlewowPathLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: paths.TurtlewowPath, Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - winerosettaDllPath := filepath.Join(paths.TurtlewowPath, "winerosetta.dll") - d3d9DllPath := filepath.Join(paths.TurtlewowPath, "d3d9.dll") - libSiliconPatchDllPath := filepath.Join(paths.TurtlewowPath, "libSiliconPatch.dll") - rosettaX87DirPath := filepath.Join(paths.TurtlewowPath, "rosettax87") - dllsTextFile := filepath.Join(paths.TurtlewowPath, "dlls.txt") - rosettaX87ExePath := filepath.Join(rosettaX87DirPath, "rosettax87") - libRuntimeRosettaX87Path := filepath.Join(rosettaX87DirPath, "libRuntimeRosettax87") - - dllsFileValid := false - if utils.PathExists(dllsTextFile) { - if fileContent, err := os.ReadFile(dllsTextFile); err == nil { - contentStr := string(fileContent) - if strings.Contains(contentStr, "winerosetta.dll") && strings.Contains(contentStr, "libSiliconPatch.dll") { - dllsFileValid = true - } - } - } - - if utils.PathExists(winerosettaDllPath) && utils.PathExists(d3d9DllPath) && utils.PathExists(libSiliconPatchDllPath) && - utils.DirExists(rosettaX87DirPath) && utils.PathExists(rosettaX87ExePath) && - utils.PathExists(libRuntimeRosettaX87Path) && dllsFileValid { - paths.PatchesAppliedTurtleWoW = true - } else { - // paths.PatchesAppliedTurtleWoW = false - } - } - turtlewowPathLabel.Refresh() - - if paths.PatchesAppliedTurtleWoW { - turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - if patchTurtleWoWButton != nil { - patchTurtleWoWButton.Disable() - } - if unpatchTurtleWoWButton != nil { - unpatchTurtleWoWButton.Enable() - } - } else { - turtlewowStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Not patched", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - if patchTurtleWoWButton != nil { - if paths.TurtlewowPath != "" { - patchTurtleWoWButton.Enable() - } else { - patchTurtleWoWButton.Disable() - } - } - if unpatchTurtleWoWButton != nil { - unpatchTurtleWoWButton.Disable() - } - } - turtlewowStatusLabel.Refresh() - - // Update Launch Button State - if launchButton != nil { - // Now requires service to be running as well - if paths.PatchesAppliedTurtleWoW && paths.PatchesAppliedCrossOver && - paths.TurtlewowPath != "" && paths.CrossoverPath != "" && service.IsServiceRunning() { - launchButton.Enable() - } else { - launchButton.Disable() - } - } - - // Update Service Status - if paths.ServiceStarting { - // Show pulsing "Starting..." when service is starting - if serviceStatusLabel != nil { - if !pulsingActive { - pulsingActive = true - go startPulsingAnimation() - } - } - if startServiceButton != nil { - startServiceButton.Disable() - } - if stopServiceButton != nil { - stopServiceButton.Disable() - } - } else if service.IsServiceRunning() { - pulsingActive = false - paths.RosettaX87ServiceRunning = true - if serviceStatusLabel != nil { - serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Running", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} - serviceStatusLabel.Refresh() - } - if startServiceButton != nil { - startServiceButton.Disable() - } - if stopServiceButton != nil { - stopServiceButton.Enable() - } - } else { - pulsingActive = false - paths.RosettaX87ServiceRunning = false - if serviceStatusLabel != nil { - serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Stopped", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} - serviceStatusLabel.Refresh() - } - if startServiceButton != nil { - if paths.TurtlewowPath != "" && paths.PatchesAppliedTurtleWoW { - startServiceButton.Enable() - } else { - startServiceButton.Disable() - } - } - if stopServiceButton != nil { - stopServiceButton.Disable() - } - } -} - -// startPulsingAnimation creates a pulsing effect for the "Starting..." text -func startPulsingAnimation() { - dots := 0 - for pulsingActive && paths.ServiceStarting { - var text string - switch dots % 4 { - case 0: - text = "Starting" - case 1: - text = "Starting." - case 2: - text = "Starting.." - case 3: - text = "Starting..." - } - - if serviceStatusLabel != nil { - fyne.DoAndWait(func() { - serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: text, Style: widget.RichTextStyle{ColorName: theme.ColorNamePrimary}}} - serviceStatusLabel.Refresh() - }) - } - - time.Sleep(500 * time.Millisecond) - dots++ - } -} - func CreateUI(myWindow fyne.Window) fyne.CanvasObject { + // Initialize UI component variables + crossoverPathLabel = widget.NewRichText() + turtlewowPathLabel = widget.NewRichText() + turtlewowStatusLabel = widget.NewRichText() + crossoverStatusLabel = widget.NewRichText() + serviceStatusLabel = widget.NewRichText() + // Load saved paths from prefs prefs, _ := utils.LoadPrefs() if prefs.TurtleWoWPath != "" { @@ -234,162 +26,39 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject { paths.CrossoverPath = prefs.CrossOverPath } - crossoverPathLabel = widget.NewRichText() - turtlewowPathLabel = widget.NewRichText() - turtlewowStatusLabel = widget.NewRichText() - crossoverStatusLabel = widget.NewRichText() - serviceStatusLabel = widget.NewRichText() - - // Load the application logo - logoResource, err := fyne.LoadResourceFromPath("Icon.png") - if err != nil { - log.Printf("Warning: could not load logo: %v", err) - } - - // Create the logo image with a fixed size - var logoImage *canvas.Image - if logoResource != nil { - logoImage = canvas.NewImageFromResource(logoResource) - logoImage.FillMode = canvas.ImageFillContain - logoImage.SetMinSize(fyne.NewSize(100, 100)) - } - - // Create a container to center the logo - var logoContainer fyne.CanvasObject - if logoImage != nil { - logoContainer = container.NewCenter(logoImage) - } else { - // If logo couldn't be loaded, add an empty space for consistent layout - logoContainer = container.NewCenter(widget.NewLabel("")) - } - - metalHudCheckbox = widget.NewCheck("Enable Metal Hud (show FPS)", func(checked bool) { - launcher.EnableMetalHud = checked - log.Printf("Metal HUD enabled: %v", launcher.EnableMetalHud) - }) - metalHudCheckbox.SetChecked(launcher.EnableMetalHud) - - showTerminalCheckbox = widget.NewCheck("Show Terminal", func(checked bool) { - // Save to preferences - prefs, _ := utils.LoadPrefs() - prefs.ShowTerminalNormally = checked - utils.SavePrefs(prefs) - log.Printf("Show terminal normally: %v", checked) - }) - showTerminalCheckbox.SetChecked(prefs.ShowTerminalNormally) - - vanillaTweaksCheckbox = widget.NewCheck("Enable vanilla-tweaks", func(checked bool) { - launcher.EnableVanillaTweaks = checked - // Save to preferences - prefs, _ := utils.LoadPrefs() - prefs.EnableVanillaTweaks = checked - utils.SavePrefs(prefs) - log.Printf("Vanilla-tweaks enabled: %v", launcher.EnableVanillaTweaks) - }) - vanillaTweaksCheckbox.SetChecked(prefs.EnableVanillaTweaks) - launcher.EnableVanillaTweaks = prefs.EnableVanillaTweaks - - // Load environment variables from preferences - if prefs.EnvironmentVariables != "" { - launcher.CustomEnvVars = prefs.EnvironmentVariables - } - - envVarsEntry = widget.NewEntry() - envVarsEntry.SetPlaceHolder(`Custom environment variables`) - envVarsEntry.SetText(launcher.CustomEnvVars) - envVarsEntry.OnChanged = func(text string) { - launcher.CustomEnvVars = text - // Save to preferences - prefs, _ := utils.LoadPrefs() - prefs.EnvironmentVariables = text - utils.SavePrefs(prefs) - log.Printf("Environment variables updated: %v", launcher.CustomEnvVars) - } - - patchTurtleWoWButton = widget.NewButton("Patch TurtleWoW", func() { - patching.PatchTurtleWoW(myWindow, UpdateAllStatuses) - }) - unpatchTurtleWoWButton = widget.NewButton("Unpatch TurtleWoW", func() { - patching.UnpatchTurtleWoW(myWindow, UpdateAllStatuses) - }) - patchCrossOverButton = widget.NewButton("Patch CrossOver", func() { - patching.PatchCrossOver(myWindow, UpdateAllStatuses) - }) - unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() { - patching.UnpatchCrossOver(myWindow, UpdateAllStatuses) - }) - startServiceButton = widget.NewButton("Start Service", func() { - service.StartRosettaX87Service(myWindow, UpdateAllStatuses) - }) - stopServiceButton = widget.NewButton("Stop Service", func() { - service.StopRosettaX87Service(myWindow, UpdateAllStatuses) - }) - launchButton = widget.NewButton("Launch Game", func() { - launcher.LaunchGame(myWindow) - }) + // Create all UI components + createOptionsComponents() + createPatchingButtons(myWindow) + createServiceButtons(myWindow) + createLaunchButton(myWindow) + // Check default CrossOver path paths.CheckDefaultCrossOverPath() - pathSelectionForm := widget.NewForm( - widget.NewFormItem("CrossOver Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { - paths.SelectCrossOverPath(myWindow, crossoverPathLabel, UpdateAllStatuses) - }), crossoverPathLabel)), - widget.NewFormItem("TurtleWoW Path:", container.NewBorder(nil, nil, nil, widget.NewButton("Set/Change", func() { - paths.SelectTurtleWoWPath(myWindow, turtlewowPathLabel, UpdateAllStatuses) - }), turtlewowPathLabel)), + // Create header, main content and bottom bar + headerContent := createHeaderContainer() + mainContent := createMainContent(myWindow) + bottomBar := createBottomBar(myWindow) + + // Initial UI state update + UpdateAllStatuses() + + // Create layout with header at top, main content moved up to avoid bottom bar, and bottom bar + // Use VBox to position main content higher up instead of centering it + mainContentContainer := container.NewVBox( + mainContent, ) - patchOperationsLayout := container.NewVBox( - widget.NewSeparator(), - container.NewGridWithColumns(4, - widget.NewLabel("TurtleWoW Patch:"), turtlewowStatusLabel, patchTurtleWoWButton, unpatchTurtleWoWButton, - ), - container.NewGridWithColumns(4, - widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, unpatchCrossOverButton, - ), - container.NewGridWithColumns(4, - widget.NewLabel("RosettaX87 Service:"), serviceStatusLabel, startServiceButton, stopServiceButton, - ), - widget.NewSeparator(), + // Add horizontal padding to the main content + paddedMainContent := container.NewPadded(mainContentContainer) + + layout := container.NewBorder( + headerContent, // top + bottomBar, // bottom + nil, // left + nil, // right + paddedMainContent, // main content with horizontal padding ) - UpdateAllStatuses() // Initial UI state update - - // Set up periodic status updates to keep service status in sync - // go func() { - // for { - // time.Sleep(5 * time.Second) // Check every 5 seconds - // fyne.DoAndWait(func() { - // UpdateAllStatuses() - // }) - // } - // }() - - // Create GitHub link - githubURL := "https://github.com/tairasu/TurtleSilicon" - parsedURL, err := url.Parse(githubURL) - if err != nil { - log.Printf("Error parsing GitHub URL: %v", err) - } - githubLink := widget.NewHyperlink("GitHub Repository", parsedURL) - githubContainer := container.NewCenter(githubLink) - - return container.NewPadded( - container.NewVBox( - logoContainer, - pathSelectionForm, - patchOperationsLayout, - container.NewGridWithColumns(3, - metalHudCheckbox, - showTerminalCheckbox, - vanillaTweaksCheckbox, - ), - widget.NewSeparator(), - widget.NewLabel("Environment Variables:"), - envVarsEntry, - container.NewPadded(launchButton), - widget.NewSeparator(), - githubContainer, - ), - ) + return layout } diff --git a/pkg/ui/variables.go b/pkg/ui/variables.go new file mode 100644 index 0000000..f22c469 --- /dev/null +++ b/pkg/ui/variables.go @@ -0,0 +1,38 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +// UI component variables - centralized for easy access across modules +var ( + // Status labels + crossoverPathLabel *widget.RichText + turtlewowPathLabel *widget.RichText + turtlewowStatusLabel *widget.RichText + crossoverStatusLabel *widget.RichText + serviceStatusLabel *widget.RichText + + // Action buttons + launchButton *widget.Button + playButton *widget.Button + playButtonText *widget.RichText + patchTurtleWoWButton *widget.Button + patchCrossOverButton *widget.Button + unpatchTurtleWoWButton *widget.Button + unpatchCrossOverButton *widget.Button + startServiceButton *widget.Button + stopServiceButton *widget.Button + + // Option checkboxes + metalHudCheckbox *widget.Check + showTerminalCheckbox *widget.Check + vanillaTweaksCheckbox *widget.Check + + // Environment variables entry + envVarsEntry *widget.Entry + + // Window reference for popup functionality + currentWindow fyne.Window +)