Files
EpochSilicon/pkg/ui/popup.go
2025-07-25 14:49:59 -07:00

361 lines
12 KiB
Go

package ui
import (
"epochsilicon/pkg/log"
"epochsilicon/pkg/patching"
"errors"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"os"
"os/exec"
"path/filepath"
"strings"
"howett.net/plist"
"epochsilicon/pkg/paths"
"epochsilicon/pkg/utils"
)
// showOptionsPopup creates and shows an integrated popup window for options
func showOptionsPopup() {
if currentWindow == nil {
return
}
// Create General tab content
generalTitle := widget.NewLabel("General Settings")
generalTitle.TextStyle = fyne.TextStyle{Bold: true}
generalContainer := container.NewVBox(
generalTitle,
widget.NewSeparator(),
advancedLoggingCheckbox,
metalHudCheckbox,
showTerminalCheckbox,
autoDeleteWdbCheckbox,
widget.NewSeparator(),
container.NewBorder(nil, nil, nil, container.NewHBox(enableOptionAsAltButton, disableOptionAsAltButton), optionAsAltStatusLabel),
)
// Create Environment Variables tab content
envVarsTitle := widget.NewLabel("Environment Variables")
envVarsTitle.TextStyle = fyne.TextStyle{Bold: true}
envVarsContainer := container.NewVBox(
envVarsTitle,
widget.NewSeparator(),
envVarsEntry,
)
// Create tabs
tabs := container.NewAppTabs(
container.NewTabItem("General", container.NewScroll(generalContainer)),
container.NewTabItem("Environment", container.NewScroll(envVarsContainer)),
)
// Set tab location to top
tabs.SetTabLocation(container.TabLocationTop)
// 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(tabs), // center
)
// Get the window size and calculate 2/3 size
windowSize := currentWindow.Content().Size()
popupWidth := windowSize.Width * 5 / 6
popupHeight := windowSize.Height * 9 / 10
// 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() {
if remapOperationInProgress {
// Show warning popup instead of closing
showRemapWarningPopup()
} else {
popup.Hide()
}
}
popup.Show()
}
// showRemapWarningPopup shows a warning popup when user tries to close options during remap operation
func showRemapWarningPopup() {
if currentWindow == nil {
return
}
// Create warning content
warningTitle := widget.NewRichTextFromMarkdown("# ⚠️ Please Wait")
warningMessage := widget.NewRichTextFromMarkdown("**Remap operation is in progress.**\n\nThe wine registry is being modified. This will take a moment.\n\nPlease wait for the operation to complete before closing the options.")
// Create OK button
okButton := widget.NewButton("OK", func() {
// This will be set when the popup is created
})
okButton.Importance = widget.HighImportance
// Create warning content container
warningContent := container.NewVBox(
container.NewCenter(warningTitle),
widget.NewSeparator(),
warningMessage,
widget.NewSeparator(),
container.NewCenter(okButton),
)
// Calculate smaller popup size
windowSize := currentWindow.Content().Size()
popupWidth := windowSize.Width * 2 / 3
popupHeight := windowSize.Height / 2
// Create the warning popup
warningPopup := widget.NewModalPopUp(container.NewPadded(warningContent), currentWindow.Canvas())
warningPopup.Resize(fyne.NewSize(popupWidth, popupHeight))
// Set the OK button action to hide the warning popup
okButton.OnTapped = func() {
warningPopup.Hide()
}
warningPopup.Show()
}
// showTroubleshootingPopup creates and shows a popup window for troubleshooting actions
func showTroubleshootingPopup() {
if currentWindow == nil {
return
}
// --- CrossOver Version Check ---
crossoverVersion := getCrossoverVersion(paths.CrossoverPath)
var crossoverStatusShort *widget.Label
var crossoverStatusDetail *widget.Label
if crossoverVersion == "" {
crossoverStatusShort = widget.NewLabel("Not found")
crossoverStatusDetail = widget.NewLabel("")
} else if isCrossoverVersionRecommended(crossoverVersion) {
crossoverStatusShort = widget.NewLabelWithStyle("✔ "+crossoverVersion, fyne.TextAlignTrailing, fyne.TextStyle{Bold: true})
crossoverStatusDetail = widget.NewLabelWithStyle("✔ Recommended version of CrossOver installed", fyne.TextAlignLeading, fyne.TextStyle{Italic: true})
} else {
crossoverStatusShort = widget.NewLabelWithStyle("⚠️ "+crossoverVersion, fyne.TextAlignTrailing, fyne.TextStyle{Italic: true})
crossoverStatusDetail = widget.NewLabelWithStyle("⚠️ Please update to CrossOver 25.0.1 or later!", fyne.TextAlignLeading, fyne.TextStyle{Italic: true})
}
crossoverStatusDetail.Wrapping = fyne.TextWrapWord
// --- Delete WDB Directory ---
wdbDeleteButton = widget.NewButton("Delete", func() {
wdbPath := filepath.Join(paths.EpochPath, "WDB")
if !utils.DirExists(wdbPath) {
dialog.ShowInformation("WDB Not Found", "No WDB directory found in your Epoch folder.", currentWindow)
return
}
dialog.NewConfirm("Delete WDB Directory", "Are you sure you want to delete the WDB directory? This will remove all cached data. No important data will be lost.", func(confirm bool) {
if confirm {
err := os.RemoveAll(wdbPath)
if err != nil {
dialog.ShowError(fmt.Errorf("Failed to delete WDB: %v", err), currentWindow)
} else {
dialog.ShowInformation("WDB Deleted", "WDB directory deleted successfully.", currentWindow)
}
}
}, currentWindow).Show()
})
// --- Delete Wine Prefixes ---
wineDeleteButton = widget.NewButton("Delete", func() {
homeDir, _ := os.UserHomeDir()
userWine := filepath.Join(homeDir, ".wine")
turtleWine := filepath.Join(paths.EpochPath, ".wine")
msg := "Are you sure you want to delete the following Wine prefixes?\n\n- " + userWine + "\n- " + turtleWine + "\n\nThis cannot be undone."
dialog.NewConfirm("Delete Wine Prefixes", msg, func(confirm bool) {
if confirm {
err1 := os.RemoveAll(userWine)
err2 := os.RemoveAll(turtleWine)
if err1 != nil && !os.IsNotExist(err1) {
dialog.ShowError(fmt.Errorf("Failed to delete ~/.wine: %v", err1), currentWindow)
return
}
if err2 != nil && !os.IsNotExist(err2) {
dialog.ShowError(fmt.Errorf("Failed to delete Epoch/.wine: %v", err2), currentWindow)
return
}
dialog.ShowInformation("Wine Prefixes Deleted", "Wine prefixes deleted successfully.", currentWindow)
}
}, currentWindow).Show()
})
// --- Build Rosettax87 Locally ---
buildRosettaButton = widget.NewButton("Build", func() {
msg := "Building rosettax87 on your computer may speed up launch times. This requires xcode-commandline-tools and Cmake to be installed. See the instructions at https://git.burkey.co/eburk/epochsilicon/README.md\n\n"
msg += "Click YES to start the build process. This could take up to a minute depending on the the speed of your Mac. A popup will let you know when the files have been built and copied to the right place."
dialog.NewConfirm("Build rosettax87", msg, func(confirm bool) {
if confirm {
// Check for dependencies
if _, err := exec.LookPath("clang"); err != nil {
m := fmt.Errorf("xcode command line tools are not installed on your computer. Click the Website button in the app and read the instructions on building rosettax87 before trying again")
log.Error(m.Error())
dialog.ShowError(m, currentWindow)
return
}
if _, err := exec.LookPath("cmake"); err != nil {
m := fmt.Errorf("Cmake is not installed on your computer. Click the Website button in the app and read the instructions on building rosettax87 before trying again")
log.Error(m.Error())
dialog.ShowError(m, currentWindow)
return
}
xPath, lPath, err := patching.BuildRosetta()
if err != nil {
m := fmt.Errorf("Error building rosettax87: %v\nClick on File Issue to upload your logs and file a support issue", err)
log.Error(m.Error())
dialog.ShowError(m, currentWindow)
return
}
d := filepath.Join(paths.EpochPath, "rosettax87")
if err = os.RemoveAll(d); err != nil {
m := fmt.Errorf("Error removing existing rosettax87 directory: %v", err)
log.Error(m.Error())
dialog.ShowError(m, currentWindow)
return
}
if err = os.MkdirAll(d, 0755); err != nil {
m := fmt.Errorf("Error creating existing rosettax87 directory: %v", err)
log.Error(m.Error())
dialog.ShowError(m, currentWindow)
return
}
pathMap := map[string]string{
xPath: filepath.Join(paths.EpochPath, "rosettax87", "rosettax87"),
lPath: filepath.Join(paths.EpochPath, "rosettax87", "libRuntimeRosettax87"),
}
for srcPath, destPath := range pathMap {
fBytes, err := os.ReadFile(srcPath)
if err != nil {
errMsg := fmt.Sprintf("failed to read source file %s: %v", srcPath, err)
dialog.ShowError(errors.New(errMsg), currentWindow)
log.Debug(errMsg)
return
}
err = os.WriteFile(destPath, fBytes, 0755)
if err != nil {
errMsg := fmt.Sprintf("failed to write file %s: %v", destPath, err)
dialog.ShowError(errors.New(errMsg), currentWindow)
log.Debug(errMsg)
return
}
log.Debugf("Successfully copied %s to %s", srcPath, destPath)
}
dialog.ShowInformation("Build Successful", "Rosettax87 installed successfully.", currentWindow)
}
}, currentWindow).Show()
})
troubleshootingTitle := widget.NewLabel("Troubleshooting")
troubleshootingTitle.TextStyle = fyne.TextStyle{Bold: true}
rowCrossover := container.NewBorder(nil, nil, widget.NewLabel("CrossOver version:"), crossoverStatusShort, nil)
rowWDB := container.NewBorder(nil, nil, widget.NewLabel("Delete WDB directory (cache):"), wdbDeleteButton, nil)
rowWine := container.NewBorder(nil, nil, widget.NewLabel("Delete Wine prefixes (~/.wine & Epoch/.wine):"), wineDeleteButton, nil)
rowBuildRosetta := container.NewBorder(nil, nil, widget.NewLabel("Build Rosettax87 locally):"), buildRosettaButton, nil)
appMgmtNote := widget.NewLabel("Please ensure EpochSilicon is enabled in System Settings > Privacy & Security > App Management.")
appMgmtNote.Wrapping = fyne.TextWrapWord
appMgmtNote.TextStyle = fyne.TextStyle{Italic: true}
content := container.NewVBox(
troubleshootingTitle,
widget.NewSeparator(),
rowCrossover,
crossoverStatusDetail,
rowWDB,
rowWine,
rowBuildRosetta,
appMgmtNote,
)
scrollContainer := container.NewScroll(content)
troubleshootingCloseButton = widget.NewButton("Close", func() {})
popupContent := container.NewBorder(
nil, // top
container.NewCenter(troubleshootingCloseButton), // bottom
nil, // left
nil, // right
container.NewPadded(scrollContainer), // center
)
windowSize := currentWindow.Content().Size()
popupWidth := windowSize.Width * 5 / 6
popupHeight := windowSize.Height * 5 / 6
popup := widget.NewModalPopUp(popupContent, currentWindow.Canvas())
popup.Resize(fyne.NewSize(popupWidth, popupHeight))
troubleshootingCloseButton.OnTapped = func() {
popup.Hide()
}
popup.Show()
}
// getCrossoverVersion reads the Info.plist and returns the version string, or "" if not found
func getCrossoverVersion(appPath string) string {
if appPath == "" {
return ""
}
plistPath := filepath.Join(appPath, "Contents", "Info.plist")
f, err := os.Open(plistPath)
if err != nil {
return ""
}
defer f.Close()
var data struct {
Version string `plist:"CFBundleShortVersionString"`
}
decoder := plist.NewDecoder(f)
if err := decoder.Decode(&data); err != nil {
return ""
}
return data.Version
}
// isCrossoverVersionRecommended returns true if version >= 25.0.1
func isCrossoverVersionRecommended(version string) bool {
parts := strings.Split(version, ".")
if len(parts) < 3 {
return false
}
major := parts[0]
minor := parts[1]
patch := parts[2]
if major > "25" {
return true
}
if major == "25" && minor >= "0" && patch >= "1" {
return true
}
return false
}