diff --git a/AUTHORS b/AUTHORS index 0de03866..c232c953 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ Andy Williams Stephen Houston -Jacob Alzén <> +Jacob Alzén +Tai Groot diff --git a/desk.go b/desk.go index 4fff54c9..a75504d8 100644 --- a/desk.go +++ b/desk.go @@ -9,6 +9,7 @@ type Desktop interface { RecentApps() []AppData Settings() DeskSettings ContentBoundsPixels(*Screen) (x, y, w, h uint32) + RootSizePixels() (w, h uint32) Screens() ScreenList IconProvider() ApplicationProvider diff --git a/internal/notify/desktop.go b/internal/notify/desktop.go new file mode 100644 index 00000000..1ed3ce33 --- /dev/null +++ b/internal/notify/desktop.go @@ -0,0 +1,6 @@ +package notify + +// DesktopNotify allows modules to be informed when user changes virtual desktop +type DesktopNotify interface { + DesktopChangeNotify(int) +} diff --git a/internal/ui/about.go b/internal/ui/about.go index 386c4048..b1539c71 100644 --- a/internal/ui/about.go +++ b/internal/ui/about.go @@ -43,10 +43,10 @@ func (w *widgetPanel) showAbout() { title := widget.NewRichTextFromMarkdown("**Version:** " + version()) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter - authors := widget.NewRichTextFromMarkdown("\n**Authors:**\n\n * Andy Williams\n * Stephen Houston\n * Jacob Alzén\n") + authors := widget.NewRichTextFromMarkdown("\n**Authors:**\n\n * Andy Williams\n * Stephen Houston\n * Jacob Alzén\n * Tai Groot\n") buttons := container.NewGridWithColumns(3, newURLButton("Home Page", "https://fyshos.com/fynedesk"), - newURLButton("Report Issue", "https://github.com/fyne-io/fynedesk/issues/new"), + newURLButton("Report Issue", "https://github.com/FyshOS/fynedesk/issues/new"), newURLButton("Sponsor", "https://github.com/sponsors/fyne-io"), ) diff --git a/internal/ui/bar.go b/internal/ui/bar.go index b62f6139..3e799583 100644 --- a/internal/ui/bar.go +++ b/internal/ui/bar.go @@ -148,6 +148,10 @@ func (b *bar) WindowAdded(win fynedesk.Window) { } } +func (b *bar) WindowMoved(_ fynedesk.Window) {} + +func (b *bar) WindowOrderChanged() {} + func (b *bar) WindowRemoved(win fynedesk.Window) { if win.Properties().SkipTaskbar() || b.desk.Settings().LauncherDisableTaskbar() { return diff --git a/internal/ui/desk.go b/internal/ui/desk.go index 460289dd..5b9c1fa9 100644 --- a/internal/ui/desk.go +++ b/internal/ui/desk.go @@ -5,13 +5,15 @@ import ( "os/exec" "strconv" - "fyshos.com/fynedesk" - wmtheme "fyshos.com/fynedesk/theme" - "fyshos.com/fynedesk/wm" - "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" deskDriver "fyne.io/fyne/v2/driver/desktop" + + "fyshos.com/fynedesk" + "fyshos.com/fynedesk/internal/notify" + wmtheme "fyshos.com/fynedesk/theme" + "fyshos.com/fynedesk/wm" ) const ( @@ -46,13 +48,35 @@ func (l *desktop) Desktop() int { } func (l *desktop) SetDesktop(id int) { + diff := id - l.desk l.desk = id - for _, item := range l.wm.Windows() { - if item.Desktop() == id { - item.Uniconify() - } else { - item.Iconify() + _, height := l.RootSizePixels() + offPix := float32(diff * -int(height)) + wins := l.wm.Windows() + + starts := make([]fyne.Position, len(wins)) + deltas := make([]fyne.Delta, len(wins)) + for i, win := range wins { + starts[i] = win.Position() + + display := l.Screens().ScreenForWindow(win) + off := offPix / display.Scale + deltas[i] = fyne.NewDelta(0, off) + } + + fyne.NewAnimation(canvas.DurationStandard, func(f float32) { + for i, item := range l.wm.Windows() { + newX := starts[i].X + deltas[i].DX*f + newY := starts[i].Y + deltas[i].DY*f + + item.Move(fyne.NewPos(newX, newY)) + } + }).Start() + + for _, m := range l.Modules() { + if desk, ok := m.(notify.DesktopNotify); ok { + desk.DesktopChangeNotify(id) } } } @@ -179,6 +203,22 @@ func (l *desktop) ContentBoundsPixels(screen *fynedesk.Screen) (x, y, w, h uint3 return 0, 0, screenW, screenH } +func (l *desktop) RootSizePixels() (w, h uint32) { + for _, screen := range l.Screens().Screens() { + right := uint32(screen.X + screen.Width) + bottom := uint32(screen.Y + screen.Height) + + if right > w { + w = right + } + if bottom > h { + h = bottom + } + } + + return w, h +} + func (l *desktop) IconProvider() fynedesk.ApplicationProvider { return l.icons } diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 3ed29bc4..31983d59 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -1,12 +1,15 @@ package ui import ( + "image/color" "os" "sort" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" deskDriver "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" @@ -49,6 +52,47 @@ func (w *widgetPanel) appendAppCategories(acc *widget.Accordion, win fyne.Window acc.Refresh() } +func (w *widgetPanel) askLogout() { + win := fyne.CurrentApp().Driver().(deskDriver.Driver).CreateSplashWindow() + logout := widget.NewButtonWithIcon("Logout", theme.LogoutIcon(), func() { + win.Close() + w.desk.WindowManager().Close() + }) + logout.Importance = widget.DangerImportance + cancel := widget.NewButton("Cancel", func() { + win.Close() + }) + + header := widget.NewRichTextFromMarkdown("### Log out") + header.Truncation = fyne.TextTruncateEllipsis + bottomPad := canvas.NewRectangle(color.Transparent) + bottomPad.SetMinSize(fyne.NewSquareSize(10)) + content := container.NewBorder( + header, + container.NewVBox( + container.NewHBox(layout.NewSpacer(), + container.NewGridWithColumns(2, cancel, logout), + layout.NewSpacer()), bottomPad), + nil, nil, + widget.NewLabel("Are you sure you want to log out?")) + + r, g, b, _ := theme.OverlayBackgroundColor().RGBA() + bgCol := &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 230} + + bg := canvas.NewRectangle(bgCol) + icon := canvas.NewImageFromResource(theme.LogoutIcon()) + iconBox := container.NewWithoutLayout(icon) + icon.Resize(fyne.NewSize(92, 92)) + icon.Move(fyne.NewPos(280-92-theme.Padding(), theme.Padding())) + win.SetContent(container.NewStack( + iconBox, bg, + container.NewPadded(content))) + + win.Resize(fyne.NewSize(280, 150)) + win.CenterOnScreen() + win.Show() +} + func (w *widgetPanel) showAccountMenu(_ fyne.CanvasObject) { w2 := fyne.CurrentApp().Driver().(deskDriver.Driver).CreateSplashWindow() w2.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) { @@ -58,8 +102,8 @@ func (w *widgetPanel) showAccountMenu(_ fyne.CanvasObject) { }) items1 := []fyne.CanvasObject{ &widget.Button{Icon: theme.LogoutIcon(), Importance: widget.DangerImportance, OnTapped: func() { + w.askLogout() w2.Close() - w.desk.WindowManager().Close() }}} isEmbed := w.desk.(*desktop).root.Title() != RootWindowName if !isEmbed { diff --git a/internal/ui/settings.go b/internal/ui/settings.go index d5887b91..8a5efae5 100644 --- a/internal/ui/settings.go +++ b/internal/ui/settings.go @@ -2,6 +2,7 @@ package ui import ( "os" + "runtime" "strings" "sync" @@ -234,13 +235,17 @@ func (d *deskSettings) load() { d.launcherZoomScale = 2.0 } - moduleNames := fyne.CurrentApp().Preferences().StringWithFallback("modulenames", "Battery|Brightness|Compositor|Sound|Launcher: Calculate|Launcher: Open URLs|Network|Virtual Desktops") + defaultModules := "Battery|Brightness|Compositor|Sound|Launcher: Calculate|Launcher: Open URLs|Network|Virtual Desktops|SystemTray" + if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { // testing + defaultModules = "Battery|Brightness|Sound|Launcher: Calculate|Launcher: Open URLs|Network|Virtual Desktops" + } + moduleNames := fyne.CurrentApp().Preferences().StringWithFallback("modulenames", defaultModules) if moduleNames != "" { d.moduleNames = strings.Split(moduleNames, "|") } d.modifier = fyne.KeyModifier(fyne.CurrentApp().Preferences().IntWithFallback("keyboardmodifier", int(fyne.KeyModifierSuper))) - d.narrowLeftLauncher = fyne.CurrentApp().Preferences().Bool("launchernarrowleft") - d.narrowPanel = fyne.CurrentApp().Preferences().Bool("narrowpanel") + d.narrowLeftLauncher = fyne.CurrentApp().Preferences().BoolWithFallback("launchernarrowleft", true) + d.narrowPanel = fyne.CurrentApp().Preferences().BoolWithFallback("narrowpanel", true) d.borderButtonPosition = fyne.CurrentApp().Preferences().StringWithFallback("borderbuttonposition", "Left") diff --git a/internal/ui/switcher.go b/internal/ui/switcher.go index 346ef16c..6bd6dcaf 100644 --- a/internal/ui/switcher.go +++ b/internal/ui/switcher.go @@ -4,6 +4,8 @@ import ( "image/color" "time" + wmTheme "fyshos.com/fynedesk/theme" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" @@ -37,6 +39,9 @@ func (s *switchIcon) CreateRenderer() fyne.WidgetRenderer { } else { res = s.win.Properties().Icon() } + if res == nil { + res = wmTheme.BrokenImageIcon + } bg := canvas.NewRectangle(color.Transparent) bg.CornerRadius = theme.InputRadiusSize() diff --git a/internal/x11/win/client.go b/internal/x11/win/client.go index 7aa97918..7e8fefd2 100644 --- a/internal/x11/win/client.go +++ b/internal/x11/win/client.go @@ -6,14 +6,15 @@ package win import ( "image" - "fyne.io/fyne/v2" - "github.com/BurntSushi/xgb/xproto" "github.com/BurntSushi/xgbutil/ewmh" "github.com/BurntSushi/xgbutil/icccm" "github.com/BurntSushi/xgbutil/xevent" "github.com/BurntSushi/xgbutil/xprop" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyshos.com/fynedesk" "fyshos.com/fynedesk/internal/x11" "fyshos.com/fynedesk/wm" @@ -125,12 +126,28 @@ func (c *client) SetDesktop(id int) { } d := fynedesk.Instance() + diff := id - c.desk c.desk = id - if id == d.Desktop() { - c.Uniconify() - } else { - c.Iconify() - } + + _, height := d.RootSizePixels() + offPix := float32(diff * -int(height)) + display := d.Screens().ScreenForWindow(c) + off := offPix / display.Scale + + start := c.Position() + fyne.NewAnimation(canvas.DurationStandard, func(f float32) { + newY := start.Y - off*f + + c.Move(fyne.NewPos(start.X, newY)) + + type moveNotifier interface { + NotifyWindowMoved(win fynedesk.Window) + } + if mover, ok := fynedesk.Instance().WindowManager().(moveNotifier); ok { + mover.NotifyWindowMoved(c) + } + + }).Start() } func (c *client) Expose() { @@ -193,6 +210,14 @@ func (c *client) Maximized() bool { return c.maximized } +func (c *client) Move(pos fyne.Position) { + screen := fynedesk.Instance().Screens().ScreenForWindow(c) + + targetX := int16(pos.X * screen.CanvasScale()) + targetY := int16(pos.Y * screen.CanvasScale()) + c.frame.updateGeometry(targetX, targetY, c.frame.width, c.frame.height, false) +} + func (c *client) NotifyBorderChange() { c.props.refreshCache() if c.Properties().Decorated() { @@ -292,6 +317,14 @@ func (c *client) Position() fyne.Position { float32(c.frame.y)/screen.CanvasScale()) } +func (c *client) Size() fyne.Size { + screen := fynedesk.Instance().Screens().ScreenForWindow(c) + + return fyne.NewSize( + float32(c.frame.width)/screen.CanvasScale(), + float32(c.frame.height)/screen.CanvasScale()) +} + func (c *client) QueueMoveResizeGeometry(x int, y int, width uint, height uint) { c.frame.queueGeometry(int16(x), int16(y), uint16(width), uint16(height), true) } diff --git a/internal/x11/win/frame.go b/internal/x11/win/frame.go index 5bcab777..b43ba0f8 100644 --- a/internal/x11/win/frame.go +++ b/internal/x11/win/frame.go @@ -40,8 +40,9 @@ type frame struct { resizeLeft, resizeRight bool moveOnly, ignoreDrag bool - borderTop, borderTopRight xproto.Pixmap - borderTopWidth uint16 + borderTop, borderTopRight xproto.Pixmap + borderTopGC, borderTopRightGC, rectGC xproto.Gcontext + borderTopWidth uint16 hovered desktop.Hoverable clickCount int @@ -305,6 +306,20 @@ func (f *frame) createPixmaps(depth byte) error { xproto.Drawable(f.client.wm.X().Screen().Root), rightWidthPix, heightPix) f.borderTopRight = pid + backR, backG, backB, _ := theme.DisabledButtonColor().RGBA() + if f.client.Focused() { + backR, backG, backB, _ = theme.BackgroundColor().RGBA() + } + bgColor := uint32(uint8(backR))<<16 | uint32(uint8(backG))<<8 | uint32(uint8(backB)) + + f.rectGC, _ = xproto.NewGcontextId(f.client.wm.Conn()) + xproto.CreateGC(f.client.wm.Conn(), f.rectGC, xproto.Drawable(f.client.id), xproto.GcForeground, []uint32{bgColor}) + + f.borderTopGC, _ = xproto.NewGcontextId(f.client.wm.Conn()) + xproto.CreateGC(f.client.wm.Conn(), f.borderTopGC, xproto.Drawable(f.borderTop), xproto.GcForeground, []uint32{bgColor}) + f.borderTopRightGC, _ = xproto.NewGcontextId(f.client.wm.Conn()) + xproto.CreateGC(f.client.wm.Conn(), f.borderTopRightGC, xproto.Drawable(f.borderTopRight), xproto.GcForeground, []uint32{bgColor}) + return nil } @@ -324,35 +339,20 @@ func (f *frame) decorate(force bool) { refresh = true } - backR, backG, backB, _ := theme.DisabledButtonColor().RGBA() - if f.client.Focused() { - backR, backG, backB, _ = theme.BackgroundColor().RGBA() - } - bgColor := uint32(uint8(backR))<<16 | uint32(uint8(backG))<<8 | uint32(uint8(backB)) - - drawTop, _ := xproto.NewGcontextId(f.client.wm.Conn()) - xproto.CreateGC(f.client.wm.Conn(), drawTop, xproto.Drawable(f.borderTop), xproto.GcForeground, []uint32{bgColor}) - drawTopRight, _ := xproto.NewGcontextId(f.client.wm.Conn()) - xproto.CreateGC(f.client.wm.Conn(), drawTopRight, xproto.Drawable(f.borderTopRight), xproto.GcForeground, []uint32{bgColor}) - if refresh || f.canvas == nil { - f.drawDecoration(f.borderTop, drawTop, f.borderTopRight, drawTopRight, depth) + f.drawDecoration(f.borderTop, f.borderTopGC, f.borderTopRight, f.borderTopRightGC, depth) } heightPix := x11.TitleHeight(x11.XWin(f.client)) - draw, _ := xproto.NewGcontextId(f.client.wm.Conn()) - xproto.CreateGC(f.client.wm.Conn(), draw, xproto.Drawable(f.client.id), xproto.GcForeground, []uint32{bgColor}) rect := xproto.Rectangle{X: 0, Y: 0, Width: f.width, Height: f.height} - xproto.PolyFillRectangleChecked(f.client.wm.Conn(), xproto.Drawable(f.client.id), draw, []xproto.Rectangle{rect}) + xproto.PolyFillRectangleChecked(f.client.wm.Conn(), xproto.Drawable(f.client.id), f.rectGC, []xproto.Rectangle{rect}) rightWidthPix := f.topRightPixelWidth() - rect = xproto.Rectangle{X: 0, Y: 0, Width: f.borderTopWidth, Height: x11.TitleHeight(x11.XWin(f.client))} - xproto.PolyFillRectangleChecked(f.client.wm.Conn(), xproto.Drawable(f.client.id), draw, []xproto.Rectangle{rect}) minWidth := f.canvas.Content().MinSize().Width widthPix := uint16(minWidth*f.canvas.Scale()) - rightWidthPix - xproto.CopyArea(f.client.wm.Conn(), xproto.Drawable(f.borderTop), xproto.Drawable(f.client.id), drawTop, + xproto.CopyArea(f.client.wm.Conn(), xproto.Drawable(f.borderTop), xproto.Drawable(f.client.id), f.borderTopGC, 0, 0, 0, 0, widthPix, heightPix) - xproto.CopyArea(f.client.wm.Conn(), xproto.Drawable(f.borderTopRight), xproto.Drawable(f.client.id), drawTopRight, + xproto.CopyArea(f.client.wm.Conn(), xproto.Drawable(f.borderTopRight), xproto.Drawable(f.client.id), f.borderTopRightGC, 0, 0, int16(f.width-rightWidthPix), 0, rightWidthPix, heightPix) } @@ -405,6 +405,19 @@ func (f *frame) freePixmaps() { xproto.FreePixmap(f.client.wm.Conn(), f.borderTopRight) f.borderTopRight = 0 } + + if f.rectGC != 0 { + xproto.FreeGC(f.client.wm.Conn(), f.rectGC) + f.rectGC = 0 + } + if f.borderTopGC != 0 { + xproto.FreeGC(f.client.wm.Conn(), f.borderTopGC) + f.borderTopGC = 0 + } + if f.borderTopRightGC != 0 { + xproto.FreeGC(f.client.wm.Conn(), f.borderTopRightGC) + f.borderTopRightGC = 0 + } } func (f *frame) getInnerWindowCoordinates(w uint16, h uint16) (uint32, uint32, uint32, uint32) { @@ -819,7 +832,7 @@ func (f *frame) topRightPixelWidth() uint16 { iconPix := x11.ButtonWidth(x11.XWin(f.client)) iconAndBorderPix := iconPix + x11.BorderWidth(x11.XWin(f.client))*2 + uint16(theme.Padding()*scale) if fynedesk.Instance().Settings().BorderButtonPosition() == "Right" { - iconAndBorderPix *= 3 + iconAndBorderPix = 3*iconAndBorderPix - uint16(theme.Padding()*scale) } return iconAndBorderPix - uint16(theme.Padding()*scale) diff --git a/internal/x11/wm/desk.go b/internal/x11/wm/desk.go index 3a612656..c047b93a 100644 --- a/internal/x11/wm/desk.go +++ b/internal/x11/wm/desk.go @@ -93,6 +93,8 @@ const ( keyCodeEnter = 108 keyCodeLeft = 113 keyCodeRight = 114 + keyCodeUp = 111 + keyCodeDown = 116 keyCodeBrightLess = 232 keyCodeBrightMore = 233 @@ -261,6 +263,14 @@ func (x *x11WM) ShowMenuOverlay(m *fyne.Menu, s fyne.Size, p fyne.Position) { pop.OnDismiss = win.Close pop.Show() pop.Resize(s) + go func() { + // TODO figure why sometimes this doesn't draw (size and minsize are correct) + // and then remove this workaround goroutine + time.Sleep(time.Second / 10) + pop.Resize(s) + time.Sleep(time.Second / 4) + pop.Resize(s) + }() x.ShowOverlay(win, s, p) } @@ -325,6 +335,10 @@ func (x *x11WM) keyNameToCode(n fyne.KeyName) xproto.Keycode { return keyCodeLeft case fyne.KeyRight: return keyCodeRight + case fyne.KeyUp: + return keyCodeUp + case fyne.KeyDown: + return keyCodeDown case deskDriver.KeyPrintScreen: return keyCodePrintScreen case fyne.KeyTab: @@ -478,20 +492,14 @@ func (x *x11WM) configureRoots() { xproto.SendEvent(x.x.Conn(), false, x.rootID, xproto.EventMaskStructureNotify, string(notifyEv.Bytes())) // we need to trigger a move so that the correct scale is picked up - err = xproto.ConfigureWindowChecked(x.x.Conn(), x.rootID, xproto.ConfigWindowX|xproto.ConfigWindowY| + xproto.ConfigureWindow(x.x.Conn(), x.rootID, xproto.ConfigWindowX|xproto.ConfigWindowY| xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, - []uint32{uint32(screen.X + 1), uint32(screen.Y + 1), uint32(screen.Width - 2), uint32(screen.Height - 2)}).Check() - if err != nil { - fyne.LogError("Configure Window Error", err) - } + []uint32{uint32(screen.X + 1), uint32(screen.Y + 1), uint32(screen.Width - 2), uint32(screen.Height - 2)}) // and then set the correct location - err = xproto.ConfigureWindowChecked(x.x.Conn(), x.rootID, xproto.ConfigWindowX|xproto.ConfigWindowY| + xproto.ConfigureWindow(x.x.Conn(), x.rootID, xproto.ConfigWindowX|xproto.ConfigWindowY| xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, - []uint32{uint32(screen.X), uint32(screen.Y), uint32(screen.Width), uint32(screen.Height)}).Check() - if err != nil { - fyne.LogError("Configure Window Error", err) - } + []uint32{uint32(screen.X), uint32(screen.Y), uint32(screen.Width), uint32(screen.Height)}) } } @@ -547,12 +555,9 @@ func (x *x11WM) configureWindow(win xproto.Window, ev xproto.ConfigureRequestEve x.configureRoots() // we added a root window, so reconfigure return } - err := xproto.ConfigureWindowChecked(x.x.Conn(), win, xproto.ConfigWindowX|xproto.ConfigWindowY| + xproto.ConfigureWindow(x.x.Conn(), win, xproto.ConfigWindowX|xproto.ConfigWindowY| xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, - []uint32{uint32(xcoord), uint32(ycoord), uint32(width), uint32(height)}).Check() - if err != nil { - fyne.LogError("Configure Window Error", err) - } + []uint32{uint32(xcoord), uint32(ycoord), uint32(width), uint32(height)}) } func (x *x11WM) destroyWindow(win xproto.Window) { @@ -613,6 +618,12 @@ func (x *x11WM) RootID() xproto.Window { return x.rootID } +func (x *x11WM) NotifyWindowMoved(win fynedesk.Window) { + for _, l := range x.listeners { + go l.WindowMoved(win) + } +} + func (x *x11WM) hideWindow(win xproto.Window) { c := x.clientForWin(win) if c == nil || win == c.FrameID() { @@ -653,11 +664,8 @@ func (x *x11WM) setActiveScreenFromWindow(win fynedesk.Window) { } func (x *x11WM) setInitialWindowAttributes(win xproto.Window) { - err := xproto.ChangeWindowAttributesChecked(x.x.Conn(), win, xproto.CwCursor, - []uint32{uint32(x11.DefaultCursor)}).Check() - if err != nil { - fyne.LogError("Set Cursor Error", err) - } + xproto.ChangeWindowAttributes(x.x.Conn(), win, xproto.CwCursor, + []uint32{uint32(x11.DefaultCursor)}) } func (x *x11WM) setupBindings() { @@ -738,7 +746,7 @@ func (x *x11WM) showWindow(win xproto.Window, parent xproto.Window) { screen := fynedesk.Instance().Screens().Primary() w, h := x.menuSize.Width*screen.CanvasScale(), x.menuSize.Height*screen.CanvasScale() mx, my := screen.X+int(x.menuPos.X*screen.CanvasScale()), screen.Y+int(x.menuPos.Y*screen.CanvasScale()) - xproto.ConfigureWindowChecked(x.Conn(), win, xproto.ConfigWindowX|xproto.ConfigWindowY| + xproto.ConfigureWindow(x.Conn(), win, xproto.ConfigWindowX|xproto.ConfigWindowY| xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, []uint32{uint32(mx), uint32(my), uint32(w), uint32(h)}) diff --git a/internal/x11/wm/events.go b/internal/x11/wm/events.go index e7537508..9ee5c7ad 100644 --- a/internal/x11/wm/events.go +++ b/internal/x11/wm/events.go @@ -234,8 +234,10 @@ func (x *x11WM) handleMouseEnter(ev xproto.EnterNotifyEvent) { func (x *x11WM) handleMouseLeave(ev xproto.LeaveNotifyEvent) { if ev.Event == x.menuID { // dismiss overlay menus on mouse out x.menuID = 0 - x.menuWin.Close() - x.menuWin = nil + if x.menuWin != nil { + x.menuWin.Close() + x.menuWin = nil + } } for _, c := range x.clients { @@ -263,6 +265,7 @@ func (x *x11WM) handleMouseMotion(ev xproto.MotionNotifyEvent) { } if ev.State&xproto.ButtonMask1 != 0 { c.(x11.XWin).NotifyMouseDrag(ev.RootX, ev.RootY) + x.NotifyWindowMoved(c) } else { c.(x11.XWin).NotifyMouseMotion(ev.RootX, ev.RootY) } @@ -333,6 +336,12 @@ func (x *x11WM) handleStateActionRequest(ev xproto.ClientMessageEvent, removeSta addState() } } + for _, c := range x.clients { + if c.(x11.XWin).ChildID() == ev.Window { + x.NotifyWindowMoved(c) + break + } + } } func (x *x11WM) handleVisibilityChange(ev xproto.VisibilityNotifyEvent) { @@ -424,4 +433,6 @@ func (x *x11WM) moveResize(moveX, moveY int16, c x11.XWin) { x.moveResizingLastX = moveX x.moveResizingLastY = moveY c.QueueMoveResizeGeometry(x.moveResizingX, x.moveResizingY, uint(w), uint(h)) + + x.NotifyWindowMoved(c) } diff --git a/internal/x11/wm/stack.go b/internal/x11/wm/stack.go index bb2680d4..87588470 100644 --- a/internal/x11/wm/stack.go +++ b/internal/x11/wm/stack.go @@ -4,10 +4,11 @@ package wm import ( - "fyne.io/fyne/v2" "github.com/BurntSushi/xgb/xproto" "github.com/BurntSushi/xgbutil/ewmh" + "fyne.io/fyne/v2" + "fyshos.com/fynedesk" "fyshos.com/fynedesk/internal/x11" ) @@ -46,6 +47,10 @@ func (s *stack) RaiseToTop(win fynedesk.Window) { wm := fynedesk.Instance().WindowManager().(*x11WM) windowClientListStackingUpdate(wm) + + for _, l := range s.listeners { + l.WindowOrderChanged() + } } func (s *stack) RemoveWindow(win fynedesk.Window) { diff --git a/internal/x11/wm/switcher.go b/internal/x11/wm/switcher.go index a2e43f52..cb37f0d5 100644 --- a/internal/x11/wm/switcher.go +++ b/internal/x11/wm/switcher.go @@ -70,7 +70,13 @@ func (x *x11WM) previousAppSwitcher() { } func (x *x11WM) showOrSelectAppSwitcher(reverse bool) { - if len(x.clients) <= 1 { + var visible []fynedesk.Window + for _, win := range x.clients { + if win.Desktop() == fynedesk.Instance().Desktop() && !win.Iconic() { + visible = append(visible, win) + } + } + if len(visible) <= 1 { return } xproto.GrabKeyboard(x.x.Conn(), true, x.x.RootWin(), xproto.TimeCurrentTime, xproto.GrabModeAsync, xproto.GrabModeAsync) diff --git a/modules/desktops/desktops.go b/modules/desktops/desktops.go index 4d518a6a..f5044055 100644 --- a/modules/desktops/desktops.go +++ b/modules/desktops/desktops.go @@ -4,6 +4,7 @@ import ( "strconv" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" "fyshos.com/fynedesk" ) @@ -20,6 +21,12 @@ type desktops struct { gui *pager } +func (d *desktops) DesktopChangeNotify(id int) { + oldID := d.current + d.current = id + d.gui.refreshFrom(oldID) +} + func (d *desktops) Destroy() { } @@ -37,13 +44,13 @@ func (d *desktops) Shortcuts() map[*fynedesk.Shortcut]func() { } } - mapping[&fynedesk.Shortcut{Name: "Switch to Previous Desktop", KeyName: fyne.KeyLeft, Modifier: fynedesk.UserModifier}] = func() { + mapping[&fynedesk.Shortcut{Name: "Switch to Previous Desktop", KeyName: fyne.KeyUp, Modifier: fynedesk.UserModifier}] = func() { if d.current == 0 { return } d.setDesktop(d.current - 1) } - mapping[&fynedesk.Shortcut{Name: "Switch to Next Desktop", KeyName: fyne.KeyRight, Modifier: fynedesk.UserModifier}] = func() { + mapping[&fynedesk.Shortcut{Name: "Switch to Next Desktop", KeyName: fyne.KeyDown, Modifier: fynedesk.UserModifier}] = func() { if d.current == deskCount-1 { return } @@ -53,13 +60,14 @@ func (d *desktops) Shortcuts() map[*fynedesk.Shortcut]func() { } func (d *desktops) StatusAreaWidget() fyne.CanvasObject { - return d.gui.ui + return container.NewStack(d.gui.buttons, d.gui.wins, d.gui.labels) } func (d *desktops) setDesktop(id int) { + oldID := d.current d.current = id fynedesk.Instance().SetDesktop(id) - d.gui.refresh() + d.gui.refreshFrom(oldID) } // newDesktops creates a new module that will manage virtual desktops and display a pager widget. diff --git a/modules/desktops/pager.go b/modules/desktops/pager.go index a95a887b..31dc2a19 100644 --- a/modules/desktops/pager.go +++ b/modules/desktops/pager.go @@ -4,47 +4,112 @@ import ( "strconv" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyshos.com/fynedesk" ) type pager struct { - ui fyne.CanvasObject + buttons, labels *fyne.Container + wins *fyne.Container } func newPager(d *desktops) *pager { - p := &pager{} + p := &pager{wins: container.NewWithoutLayout()} - items := make([]fyne.CanvasObject, deskCount) + buttons := make([]fyne.CanvasObject, deskCount) + labels := make([]fyne.CanvasObject, deskCount) for i := 0; i < deskCount; i++ { id := strconv.Itoa(i + 1) deskID := i - items[i] = widget.NewButton(id, func() { + buttons[i] = widget.NewButton("", func() { d.setDesktop(deskID) }) + labels[i] = widget.NewLabelWithStyle(id, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) } if fynedesk.Instance() != nil && fynedesk.Instance().Settings().NarrowWidgetPanel() { - p.ui = container.NewGridWithColumns(1, items...) + p.buttons = container.NewGridWithColumns(1, buttons...) + p.labels = container.NewGridWithColumns(1, labels...) } else { - p.ui = container.NewGridWithColumns(4, items...) + p.buttons = container.NewGridWithColumns(4, buttons...) + p.labels = container.NewGridWithColumns(4, labels...) } p.refresh() + fynedesk.Instance().WindowManager().AddStackListener(p) return p } +func (p *pager) WindowAdded(_ fynedesk.Window) { + p.refresh() +} + +func (p *pager) WindowMoved(_ fynedesk.Window) { + p.refresh() +} + +func (p *pager) WindowOrderChanged() { + p.refresh() +} + +func (p *pager) WindowRemoved(_ fynedesk.Window) { + p.refresh() +} + func (p *pager) refresh() { desk := fynedesk.Instance() + p.refreshFrom(desk.Desktop()) +} + +func (p *pager) refreshFrom(oldID int) { + desk := fynedesk.Instance() + wins := fynedesk.Instance().WindowManager().Windows() - for i, b := range p.ui.(*fyne.Container).Objects { + var rects []fyne.CanvasObject + for i, b := range p.buttons.Objects { + l := p.labels.Objects[i] if i == desk.Desktop() { b.(*widget.Button).Importance = widget.HighImportance + l.(*widget.Label).Importance = widget.LowImportance } else { b.(*widget.Button).Importance = widget.MediumImportance + l.(*widget.Label).Importance = widget.MediumImportance } + b.Refresh() + l.Refresh() + } + pivot := p.buttons.Objects[oldID] + + for j := len(wins) - 1; j >= 0; j-- { + win := wins[j] + if win.Iconic() { + continue + } + + yPad := theme.Padding() * float32(win.Desktop()-oldID) + screen := fynedesk.Instance().Screens().ScreenForWindow(win) + + var obj fyne.CanvasObject + obj = canvas.NewRectangle(theme.DisabledColor()) + if win.Properties().Icon() != nil { + obj = container.NewStack(obj, + canvas.NewImageFromResource(win.Properties().Icon())) + } + rects = append(rects, obj) + + x := (win.Position().X * screen.Scale) / float32(screen.Width) * pivot.Size().Width + y := (win.Position().Y * screen.Scale) / float32(screen.Height) * pivot.Size().Height + w := (win.Size().Width * screen.Scale) / float32(screen.Width) * pivot.Size().Width + h := (win.Size().Height * screen.Scale) / float32(screen.Height) * pivot.Size().Height + obj.Resize(fyne.NewSize(w, h)) + obj.Move(pivot.Position().Add(fyne.NewPos(x, y+yPad))) } + + p.wins.Objects = rects + p.wins.Refresh() } diff --git a/test/desktop.go b/test/desktop.go index 27529e6c..fba0eb5f 100644 --- a/test/desktop.go +++ b/test/desktop.go @@ -45,7 +45,12 @@ func (*Desktop) Capture() image.Image { // ContentBoundsPixels returns a default value for how much space maximised apps should use func (*Desktop) ContentBoundsPixels(_ *fynedesk.Screen) (x, y, w, h uint32) { - return 0, 0, uint32(320), uint32(240) + return 0, 0, 320, 240 +} + +// RootSizePixels returns the total number of pixels required to fit all the screens +func (*Desktop) RootSizePixels() (w, h uint32) { + return 320, 240 } // Desktop returns the index of the current desktop (in test this is always 0) diff --git a/test/window.go b/test/window.go index f518d801..29955cac 100644 --- a/test/window.go +++ b/test/window.go @@ -3,8 +3,9 @@ package test import ( "image" - "fyne.io/fyne/v2" "fyshos.com/fynedesk" + + "fyne.io/fyne/v2" ) // Window is an in-memory virtual window for test purposes @@ -80,6 +81,9 @@ func (w *Window) Maximized() bool { return false } +// Move the window, does nothing in test windows +func (w *Window) Move(fyne.Position) {} + // Parent returns a window that this should be positioned within, if set. func (w *Window) Parent() fynedesk.Window { return w.parent @@ -90,6 +94,9 @@ func (w *Window) Position() fyne.Position { return fyne.NewPos(0, 0) } +// Size returns 0x0 for test windows +func (w *Window) Size() fyne.Size { return fyne.Size{} } + // Properties obtains the window properties currently set func (w *Window) Properties() fynedesk.WindowProperties { return w.props diff --git a/window.go b/window.go index 1c5c6758..47699e35 100644 --- a/window.go +++ b/window.go @@ -30,6 +30,8 @@ type Window interface { Parent() Window Properties() WindowProperties // Request the properties set on this window Position() fyne.Position + Size() fyne.Size + Move(position fyne.Position) Desktop() int SetDesktop(int) diff --git a/wm.go b/wm.go index 366f631a..618fbbd4 100644 --- a/wm.go +++ b/wm.go @@ -36,4 +36,7 @@ type Stack interface { type StackListener interface { WindowAdded(Window) WindowRemoved(Window) + + WindowMoved(Window) + WindowOrderChanged() }