diff --git a/FyneApp.toml b/FyneApp.toml index 9fa0938..bd9ab98 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -4,5 +4,5 @@ Website = "https://github.com/adamk33n3r/GoBorderless" Icon = "res/icon.ico" Name = "GoBorderless" ID = "com.adamk33n3r.goborderless" - Version = "1.3.1" - Build = 79 + Version = "1.3.2" + Build = 81 diff --git a/assets/fullscreen.svg b/assets/fullscreen.svg new file mode 100644 index 0000000..ab31caf --- /dev/null +++ b/assets/fullscreen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/halfleft.svg b/assets/halfleft.svg new file mode 100644 index 0000000..d36a30e --- /dev/null +++ b/assets/halfleft.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/halfright.svg b/assets/halfright.svg new file mode 100644 index 0000000..d36a30e --- /dev/null +++ b/assets/halfright.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/go.sum b/go.sum index 4288ad5..4a397d6 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/adamk33n3r/fyne/v2 v2.7.0 h1:S5OCzr/l4GNY7SEdJBP1s89NPT9qoJLqMLm5jttcnyQ= @@ -14,8 +12,6 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM= @@ -28,14 +24,10 @@ github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw= github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= -github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= -github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= @@ -46,14 +38,12 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= -github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= -github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= -github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc= -github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 h1:vFdvrlsVU+p/KFBWTq0lTG4fvWvG88sawGlCzM+RUEU= github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -62,8 +52,6 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+ github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= -github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -80,23 +68,15 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gui_app.go b/gui_app.go index 9d0e67e..69d9bc8 100644 --- a/gui_app.go +++ b/gui_app.go @@ -42,6 +42,16 @@ func intValidator(s string) error { return nil } +func offsetIntValidator(s string) error { + if s == "" { + return nil + } + if _, err := strconv.Atoi(s); err != nil { + return fmt.Errorf("invalid number") + } + return nil +} + var settingsList *widget.List var currentWindows = make([]Window, 0) // Temporary list to store window titles diff --git a/gui_appsetting.go b/gui_appsetting.go index 287199e..c43d028 100644 --- a/gui_appsetting.go +++ b/gui_appsetting.go @@ -1,6 +1,7 @@ package main import ( + _ "embed" "fmt" "reflect" "slices" @@ -29,6 +30,10 @@ var ( widthText *widget.Entry heightText *widget.Entry confirmButton *widget.Button + + halfLeftBtn *widget.Button + halfRightBtn *widget.Button + fullBtn *widget.Button ) func isValid(isNew bool) bool { @@ -71,46 +76,23 @@ func getWindowsForSelect(allWindows []Window) []Window { // This will also filter out windows that we've already removed borders from // Perhaps we should also check the list of existing configs? for _, window := range allWindows { - style := getWindowStyle(window.hwnd) - if style&win.WS_CAPTION > 0 && - ((style&win.WS_BORDER) > 0 || (style&win.WS_THICKFRAME) > 0) { + if isValidWindowForSelection(window) { copyOfWindows = append(copyOfWindows, window) } } - slices.SortFunc(copyOfWindows, func(a Window, b Window) int { + slices.SortFunc(copyOfWindows, func(a, b Window) int { return strings.Compare(strings.ToLower(a.String()), strings.ToLower(b.String())) }) return copyOfWindows } -func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool, parent fyne.Window, onClose func(newSetting *AppSetting)) *dialog.CustomDialog { - currentWindowsMutex.Lock() - windowsForSelect := getWindowsForSelect(currentWindows) - currentWindowsMutex.Unlock() - - var appSettingDialog *dialog.CustomDialog - var windowSub rx.Subscription - - confirmButton = widget.NewButtonWithIcon("Create", theme.ConfirmIcon(), func() { - if isNew { - windowSub.Unsubscribe() - } - appSettingDialog.Hide() - onClose(&appSetting) - }) - confirmButton.Importance = widget.HighImportance - confirmButton.Disable() - cancelButton := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() { - if isNew { - windowSub.Unsubscribe() - } - appSettingDialog.Hide() - onClose(nil) - }) - if !isNew { - confirmButton.SetText("Save") - } +func isValidWindowForSelection(window Window) bool { + style := getWindowStyle(window.hwnd) + return style&win.WS_CAPTION > 0 && + ((style&win.WS_BORDER) > 0 || (style&win.WS_THICKFRAME) > 0) +} +func createApplicationSelect(windowsForSelect []Window, appSetting *AppSetting, isNew bool) *ui.Select[Window] { applicationSelect = ui.NewSelect(windowsForSelect, func(selected Window) { if slices.Index(windowsForSelect, selected) == -1 { fmt.Println("Selected application no longer exists in the updated window list, resetting selection.") @@ -125,26 +107,40 @@ func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool, }) applicationSelect.PlaceHolder = "Select Application" - monitorIdx := appSetting.Monitor - 1 - if isNew { - monitorIdx = settings.Defaults.Monitor - 1 - if monitorIdx < 0 { - monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool { - return m.isPrimary - }) - } + return applicationSelect +} + +func getDefaultMonitorIndex(settings *Settings, appSetting AppSetting, isNew bool) int { + if !isNew { + return appSetting.Monitor - 1 } + + monitorIdx := settings.Defaults.Monitor - 1 + if monitorIdx < 0 { + monitorIdx = slices.IndexFunc(monitors, func(m Monitor) bool { + return m.isPrimary + }) + } + + return monitorIdx +} + +func createDisplaySelect(settings *Settings, appSetting *AppSetting, isNew bool) *ui.Select[Monitor] { + monitorIdx := getDefaultMonitorIndex(settings, *appSetting, isNew) + displaySelect = ui.NewSelect(monitors, func(selected Monitor) { appSetting.Monitor = selected.number - setConfirmButtonState(isNew) }) displaySelect.PlaceHolder = "Select Display" displaySelect.SetSelectedIndex(monitorIdx) + return displaySelect +} + +func createMatchTypeRadio(settings *Settings, appSetting *AppSetting, isNew bool) *widget.RadioGroup { matchType = widget.NewRadioGroup(matchTypes, func(selected string) { appSetting.MatchType = GetMatchTypeFromString(selected) - setConfirmButtonState(isNew) }) if isNew { @@ -155,89 +151,101 @@ func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool, matchType.Horizontal = true matchType.Required = true - // Textboxes with labels - xOffsetLabel := widget.NewLabel("X Offset:") - xOffsetText = widget.NewEntry() - xOffsetText.Validator = intValidator - xOffsetText.OnChanged = func(s string) { - appSetting.OffsetX = entryTextToInt(s) + return matchType +} - setConfirmButtonState(isNew) +func createSizeButton(monitor Monitor, xDivider int32, xOffset int32, label string, icon fyne.Resource) *widget.Button { + monitorWidth := monitor.width / xDivider + offsetWidth := int32(0) + if xOffset == 1 { + offsetWidth = monitorWidth } - setOnFocusChanged(xOffsetText, func(focused bool) { - if focused { - xOffsetText.DoubleTapped(&fyne.PointEvent{}) - } + + return widget.NewButtonWithIcon(label, icon, func() { + widthText.SetText(strconv.Itoa(int(monitorWidth))) + heightText.SetText(strconv.Itoa(int(monitor.height))) + xOffsetText.SetText(strconv.Itoa(int(offsetWidth))) + yOffsetText.SetText("0") }) - xOffsetText.SetPlaceHolder("0") - if isNew { - xOffsetText.SetText(strconv.Itoa(int(settings.Defaults.OffsetX))) - } else { - xOffsetText.SetText(strconv.Itoa(int(appSetting.OffsetX))) - } +} - yOffsetLabel := widget.NewLabel("Y Offset:") - yOffsetText = widget.NewEntry() - yOffsetText.Validator = intValidator - yOffsetText.OnChanged = func(s string) { - appSetting.OffsetY = entryTextToInt(s) +func createOffsetEntry(label string, defaultValue int32, settings *Settings, appSetting *AppSetting, isNew bool, updateField func(int32)) (*widget.Label, *widget.Entry) { + labelWidget := widget.NewLabel(label) + entry := widget.NewEntry() + entry.Validator = offsetIntValidator + entry.OnChanged = func(s string) { + if s == "" { + updateField(0) + } else { + updateField(entryTextToInt(s)) + } setConfirmButtonState(isNew) } - setOnFocusChanged(yOffsetText, func(focused bool) { + setOnFocusChanged(entry, func(focused bool) { if focused { - yOffsetText.DoubleTapped(&fyne.PointEvent{}) + entry.DoubleTapped(&fyne.PointEvent{}) } }) - yOffsetText.SetPlaceHolder("0") + + entry.SetPlaceHolder("0") if isNew { - yOffsetText.SetText(strconv.Itoa(int(settings.Defaults.OffsetY))) + entry.SetText(strconv.Itoa(int(defaultValue))) } else { - yOffsetText.SetText(strconv.Itoa(int(appSetting.OffsetY))) + entry.SetText(strconv.Itoa(int(defaultValue))) } - widthLabel := widget.NewLabel("Width:") - widthText = widget.NewEntry() - widthText.Validator = intValidator - widthText.OnChanged = func(s string) { - appSetting.Width = entryTextToInt(s) + return labelWidget, entry +} + +func createSizeEntry(label string, placeholder string, defaultValue int32, settings *Settings, appSetting *AppSetting, isNew bool, updateField func(int32)) (*widget.Label, *widget.Entry) { + labelWidget := widget.NewLabel(label) + entry := widget.NewEntry() + entry.Validator = intValidator + entry.OnChanged = func(s string) { + updateField(entryTextToInt(s)) setConfirmButtonState(isNew) } - setOnFocusChanged(widthText, func(focused bool) { + + setOnFocusChanged(entry, func(focused bool) { if focused { - widthText.DoubleTapped(&fyne.PointEvent{}) + entry.DoubleTapped(&fyne.PointEvent{}) } }) - widthText.SetPlaceHolder("1920") + + entry.SetPlaceHolder(placeholder) if isNew { - widthText.SetText(strconv.Itoa(int(settings.Defaults.Width))) + entry.SetText(strconv.Itoa(int(defaultValue))) } else { - widthText.SetText(strconv.Itoa(int(appSetting.Width))) + entry.SetText(strconv.Itoa(int(defaultValue))) } - heightLabel := widget.NewLabel("Height:") - heightText = widget.NewEntry() - heightText.Validator = intValidator - heightText.OnChanged = func(s string) { - appSetting.Height = entryTextToInt(s) + return labelWidget, entry +} - setConfirmButtonState(isNew) - } - setOnFocusChanged(heightText, func(focused bool) { - if focused { - heightText.DoubleTapped(&fyne.PointEvent{}) - } +func createTextGrid(settings *Settings, appSetting *AppSetting, isNew bool) *fyne.Container { + xOffsetLabel, xOffsetEntry := createOffsetEntry("X Offset:", settings.Defaults.OffsetX, settings, appSetting, isNew, func(val int32) { + appSetting.OffsetX = val }) - heightText.SetPlaceHolder("1080") - if isNew { - heightText.SetText(strconv.Itoa(int(settings.Defaults.Height))) - } else { - heightText.SetText(strconv.Itoa(int(appSetting.Height))) - } + xOffsetText = xOffsetEntry - // 2x2 grid for labeled textboxes - textGrid := container.NewGridWithRows(2, + yOffsetLabel, yOffsetEntry := createOffsetEntry("Y Offset:", settings.Defaults.OffsetY, settings, appSetting, isNew, func(val int32) { + appSetting.OffsetY = val + }) + yOffsetText = yOffsetEntry + + widthLabel, widthEntry := createSizeEntry("Width:", "1920", settings.Defaults.Width, settings, appSetting, isNew, func(val int32) { + appSetting.Width = val + }) + widthText = widthEntry + + heightLabel, heightEntry := createSizeEntry("Height:", "1080", settings.Defaults.Height, settings, appSetting, isNew, func(val int32) { + appSetting.Height = val + }) + heightText = heightEntry + + return container.NewGridWithRows(2, container.NewGridWithColumns(2, container.NewVBox(xOffsetLabel, xOffsetText), container.NewVBox(yOffsetLabel, yOffsetText), @@ -247,40 +255,121 @@ func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool, container.NewVBox(heightLabel, heightText), ), ) +} - if isNew { - fmt.Println("subscribing to windows observable") - // TODO: make it work like subject where it outputs last received data on subscription +func subscribeToWindowUpdates(windowsForSelect []Window, isNew bool) rx.Subscription { + if !isNew { + return rx.Subscription{} + } + + fmt.Println("subscribing to windows observable") + // TODO: make it work like subject where it outputs last received data on subscription + + return windowObs.Subscribe(func(windows []Window) { + if len(windows) == 0 { + // This is probably a fluke, so let's skip it + return + } + + fyne.Do(func() { + windowsForSelect = getWindowsForSelect(windows) + applicationSelect.SetOptions(windowsForSelect) - windowSub = windowObs.Subscribe(func(windows []Window) { - if len(windows) == 0 { - // This is probably a fluke, so let's skip it - return + if applicationSelect.Selected != nil && slices.Index(windowsForSelect, *applicationSelect.Selected) == -1 { + fmt.Println("Selected application no longer exists in the updated window list, resetting selection.") + applicationSelect.ClearSelected() } - fyne.Do(func() { - windowsForSelect = getWindowsForSelect(windows) - applicationSelect.SetOptions(windowsForSelect) - - if applicationSelect.Selected != nil && slices.Index(windowsForSelect, *applicationSelect.Selected) == -1 { - fmt.Println("Selected application no longer exists in the updated window list, resetting selection.") - applicationSelect.ClearSelected() - } - }) }) - } + }) +} + +func createPresetsContent(settings *Settings, appSetting *AppSetting, isNew bool, cancelButton, confirmBtn *widget.Button) *fyne.Container { + presetsRow := container.NewCenter( + container.NewHBox( + halfLeftBtn, + widget.NewLabel(" "), + halfRightBtn, + widget.NewLabel(" "), + fullBtn, + ), + ) content := container.NewVBox( displaySelect, widget.NewLabel("Match Type"), matchType, - textGrid, + widget.NewLabel("Presets:"), + presetsRow, + createTextGrid(settings, appSetting, isNew), widget.NewLabel(""), // spacer - container.NewHBox(cancelButton, layout.NewSpacer(), confirmButton), + container.NewHBox(cancelButton, layout.NewSpacer(), confirmBtn), ) + if isNew { content.Objects = append([]fyne.CanvasObject{applicationSelect}, content.Objects...) } + return content +} + +//go:embed assets/fullscreen.svg +var fullscreenSVGBytes []byte + +//go:embed assets/halfleft.svg +var halfLeftSVGBytes []byte + +//go:embed assets/halfright.svg +var halfRightSVGBytes []byte + +func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool, parent fyne.Window, onClose func(newSetting *AppSetting)) *dialog.CustomDialog { + currentWindowsMutex.Lock() + windowsForSelect := getWindowsForSelect(currentWindows) + currentWindowsMutex.Unlock() + + var appSettingDialog *dialog.CustomDialog + var windowSub rx.Subscription + + leftIcon := fyne.NewStaticResource("right.svg", halfLeftSVGBytes) + rightIcon := fyne.NewStaticResource("right.svg", halfRightSVGBytes) + fullIcon := fyne.NewStaticResource("full.svg", fullscreenSVGBytes) + + monitorIdx := getDefaultMonitorIndex(settings, appSetting, isNew) + selectedMonitor := monitors[monitorIdx] + + halfLeftBtn = createSizeButton(selectedMonitor, 2, 0, "Half Left", leftIcon) + halfRightBtn = createSizeButton(selectedMonitor, 2, 1, "Half Right", rightIcon) + fullBtn = createSizeButton(selectedMonitor, 1, 0, "Full", fullIcon) + + confirmButton = widget.NewButtonWithIcon("Create", theme.ConfirmIcon(), func() { + if isNew { + windowSub.Unsubscribe() + } + appSettingDialog.Hide() + onClose(&appSetting) + }) + confirmButton.Importance = widget.HighImportance + confirmButton.Disable() + + if !isNew { + confirmButton.SetText("Save") + } + + cancelButton := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() { + if isNew { + windowSub.Unsubscribe() + } + appSettingDialog.Hide() + onClose(nil) + }) + + applicationSelect = createApplicationSelect(windowsForSelect, &appSetting, isNew) + displaySelect = createDisplaySelect(settings, &appSetting, isNew) + matchType = createMatchTypeRadio(settings, &appSetting, isNew) + + windowSub = subscribeToWindowUpdates(windowsForSelect, isNew) + + content := createPresetsContent(settings, &appSetting, isNew, cancelButton, confirmButton) + dialogName := "New App Config" if !isNew { dialogName = appSetting.Display() diff --git a/winapi.go b/winapi.go index 2659456..11dd143 100644 --- a/winapi.go +++ b/winapi.go @@ -107,24 +107,73 @@ func (m Monitor) String() string { if m.isPrimary { str += " (Primary)" } + str += fmt.Sprintf(" | %dx%d", m.width, m.height) return str } +var ( + procEnumDisplaySettingsW = user32.NewProc("EnumDisplaySettingsW") +) + +type DEVMODE struct { + DmDeviceName [32]uint16 + DmSpecVersion uint16 + DmDriverVersion uint16 + DmSize uint16 + DmDriverExtra uint16 + DmFields uint32 + DmPosition struct{ X, Y int32 } + DmDisplayOrientation uint32 + DmDisplayFixedOutput uint32 + DmColor int16 + DmDuplex int16 + DmYResolution int16 + DmTTOption int16 + DmCollate int16 + DmFormName [32]uint16 + DmLogPixels uint16 + DmBitsPerPel uint32 + DmPelsWidth uint32 + DmPelsHeight uint32 +} + func getMonitors() []Monitor { var monitors []Monitor index := 0 + cb := syscall.NewCallback(func(hMonitor win.HMONITOR, hdcMonitor win.HDC, lprcMonitor *win.RECT, dwData uintptr) uintptr { - var info win.MONITORINFO - info.CbSize = uint32(unsafe.Sizeof(info)) - if win.GetMonitorInfo(hMonitor, &info) { + var infoEx struct { + win.MONITORINFO + SzDevice [win.CCHDEVICENAME]uint16 + } + infoEx.CbSize = uint32(unsafe.Sizeof(infoEx)) + + if win.GetMonitorInfo(hMonitor, (*win.MONITORINFO)(unsafe.Pointer(&infoEx))) { + var devMode DEVMODE + devMode.DmSize = uint16(unsafe.Sizeof(devMode)) + + ret, _, _ := procEnumDisplaySettingsW.Call( + uintptr(unsafe.Pointer(&infoEx.SzDevice[0])), + uintptr(0xFFFFFFFF), + uintptr(unsafe.Pointer(&devMode)), + ) + + width := int32(devMode.DmPelsWidth) + height := int32(devMode.DmPelsHeight) + + if ret == 0 || width == 0 || height == 0 { + width = infoEx.RcMonitor.Right - infoEx.RcMonitor.Left + height = infoEx.RcMonitor.Bottom - infoEx.RcMonitor.Top + } + index++ monitors = append(monitors, Monitor{ number: index, - isPrimary: info.DwFlags&win.MONITORINFOF_PRIMARY != 0, - width: info.RcMonitor.Right - info.RcMonitor.Left, - height: info.RcMonitor.Bottom - info.RcMonitor.Top, - left: info.RcMonitor.Left, - top: info.RcMonitor.Top, + isPrimary: infoEx.DwFlags&win.MONITORINFOF_PRIMARY != 0, + width: width, + height: height, + left: infoEx.RcMonitor.Left, + top: infoEx.RcMonitor.Top, }) } return 1