Skip to content

Commit

Permalink
Merge pull request #141 from fyne-io/feature/printscreen
Browse files Browse the repository at this point in the history
Feature/printscreen
  • Loading branch information
andydotxyz authored Dec 15, 2020
2 parents e9f6a9f + 5d1c87b commit 4da4414
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 19 deletions.
5 changes: 5 additions & 0 deletions internal/ui/desk.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ func (l *desktop) registerShortcuts() {
func() {
// dummy - the wm handles app switcher
})
fynedesk.Instance().AddShortcut(&fynedesk.Shortcut{Name: "Print Window", KeyName: deskDriver.KeyPrintScreen,
Modifier: deskDriver.ShiftModifier},
l.screenshotWindow)
fynedesk.Instance().AddShortcut(&fynedesk.Shortcut{Name: "Print Screen", KeyName: deskDriver.KeyPrintScreen},
l.screenshot)
}

// Screens returns the screens provider of the current desktop environment for access to screen functionality.
Expand Down
6 changes: 6 additions & 0 deletions internal/ui/embed_wm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ui

import (
"image"

"fyne.io/fyne"

"fyne.io/fynedesk"
Expand Down Expand Up @@ -52,6 +54,10 @@ func (e *embededWM) Blank() {
// no-op, we don't control screen brightness
}

func (e *embededWM) Capture() image.Image {
return nil // would mean accessing the underling OS screen functions...
}

func (e *embededWM) Close() {
windows := fyne.CurrentApp().Driver().AllWindows()
if len(windows) > 0 {
Expand Down
73 changes: 73 additions & 0 deletions internal/ui/screenshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ui

import (
"image"
"image/png"

"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/container"
"fyne.io/fyne/dialog"
"fyne.io/fyne/layout"
"fyne.io/fyne/storage"
"fyne.io/fyne/widget"
)

func (l *desktop) screenshot() {
bg := l.wm.Capture()
l.showCaptureSave(bg)
}

func (l *desktop) screenshotWindow() {
win := l.wm.TopWindow()
if win == nil {
fyne.LogError("Unable to print window with no window visible", nil)
return
}

img := win.Capture()
if img == nil {
return
}
l.showCaptureSave(img)
}

func (l *desktop) showCaptureSave(img image.Image) {
w := fyne.CurrentApp().NewWindow("Screenshot")
save := widget.NewButton("Save...", func() {
saveImage(img, w)
})
save.Importance = widget.HighImportance
buttons := container.NewHBox(
layout.NewSpacer(),
widget.NewButton("Cancel", func() {
w.Close()
}),
save)

preview := canvas.NewImageFromImage(img)
preview.FillMode = canvas.ImageFillContain
w.SetContent(container.NewBorder(nil, buttons, nil, nil, preview))
w.Resize(fyne.NewSize(480, 360))
w.Show()
}

func saveImage(pix image.Image, w fyne.Window) {
d := dialog.NewFileSave(func(write fyne.URIWriteCloser, err error) {
if write == nil { // cancelled
return
}
if err != nil {
dialog.ShowError(err, w)
}

err = png.Encode(write, pix)
if err != nil {
dialog.ShowError(err, w)
}

w.Close()
}, w)
d.SetFilter(storage.NewMimeTypeFileFilter([]string{"image/png"}))
d.Show()
}
48 changes: 48 additions & 0 deletions internal/x11/screenshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// +build linux openbsd freebsd netbsd

package x11

import (
"image"
"math"

"fyne.io/fyne"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)

// CaptureWindow allows x11 code to get the image representation of a screen area.
// The window specified will be captured according to its bounds.
func CaptureWindow(conn *xgb.Conn, win xproto.Window) *image.NRGBA {
geom, err := xproto.GetGeometry(conn, xproto.Drawable(win)).Reply()
if err != nil {
fyne.LogError("Unable to get screen geometry", err)
return nil
}
pix, err := xproto.GetImage(conn, xproto.ImageFormatZPixmap, xproto.Drawable(win),
0, 0, geom.Width, geom.Height, math.MaxUint32).Reply()
if err != nil {
fyne.LogError("Error capturing window content", err)
return nil
}

img := image.NewNRGBA(image.Rect(0, 0, int(geom.Width), int(geom.Height)))
i := 0
for y := 0; y < int(geom.Height); y++ {
for x := 0; x < int(geom.Width); x++ {
copyPixel(pix.Data, img.Pix, i)
i += 4
}
}
return img
}

func copyPixel(in []byte, out []uint8, i int) {
b := in[i]
g := in[i+1]
r := in[i+2]
out[i] = r
out[i+1] = g
out[i+2] = b
out[i+3] = 0xff // some colour maps / depths don't include alpha
}
20 changes: 20 additions & 0 deletions internal/x11/screenshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// +build linux openbsd freebsd netbsd

package x11

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCopyPixel(t *testing.T) {
in := []byte{0xff, 0x99, 0x33, 0xff}
out := []uint8{0, 0, 0, 0}
copyPixel(in, out, 0)

assert.Equal(t, uint8(0x33), out[0])
assert.Equal(t, uint8(0x99), out[1])
assert.Equal(t, uint8(0xff), out[2])
assert.Equal(t, uint8(0xff), out[3])
}
6 changes: 6 additions & 0 deletions internal/x11/win/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package win

import (
"image"

"fyne.io/fyne"

"github.com/BurntSushi/xgb/xproto"
Expand Down Expand Up @@ -63,6 +65,10 @@ func NewClient(win xproto.Window, wm x11.XWM) x11.XWin {
return c
}

func (c *client) Capture() image.Image {
return x11.CaptureWindow(c.wm.Conn(), c.FrameID())
}

func (c *client) ChildID() xproto.Window {
return c.win
}
Expand Down
2 changes: 1 addition & 1 deletion internal/x11/win/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func (f *frame) copyDecorationPixels(width, height, xoff, yoff uint32, img image
data[i] = byte(b)
data[i+1] = byte(g)
data[i+2] = byte(r)
data[i+3] = 0
data[i+3] = 0xff

i += 4
}
Expand Down
19 changes: 13 additions & 6 deletions internal/x11/wm/desk.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ const (
moveResizeMoveKeyboard moveResizeType = 10
moveResizeCancel moveResizeType = 11

keyCodeEscape = 9
keyCodeTab = 23
keyCodeReturn = 36
keyCodeAlt = 64
keyCodeSpace = 65
keyCodeEscape = 9
keyCodeTab = 23
keyCodeReturn = 36
keyCodeAlt = 64
keyCodeSpace = 65
keyCodePrintScreen = 107

keyCodeEnter = 108
keyCodeLeft = 113
Expand Down Expand Up @@ -155,6 +156,11 @@ func (x *x11WM) Blank() {
}()
}

func (x *x11WM) Capture() image.Image {
root := x.x.RootWin()
return x11.CaptureWindow(x.x.Conn(), root)
}

func (x *x11WM) Close() {
for _, child := range x.clients {
child.Close()
Expand Down Expand Up @@ -235,6 +241,8 @@ func (x *x11WM) keyNameToCode(n fyne.KeyName) xproto.Keycode {
switch n {
case fyne.KeySpace:
return keyCodeSpace
case deskDriver.KeyPrintScreen:
return keyCodePrintScreen
case fyne.KeyTab:
return keyCodeTab
case fynedesk.KeyBrightnessDown:
Expand Down Expand Up @@ -265,7 +273,6 @@ func (x *x11WM) modifierToKeyMask(m deskDriver.Modifier) uint16 {
}

func (x *x11WM) runLoop() {
x.setupBindings()
conn := x.x.Conn()

for {
Expand Down
7 changes: 7 additions & 0 deletions test/desktop.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package test

import (
"image"

"fyne.io/fyne"
"fyne.io/fyne/test"

Expand Down Expand Up @@ -34,6 +36,11 @@ func (*Desktop) AddShortcut(shortcut *fynedesk.Shortcut, handler func()) {
// TODO
}

// Capture the desktop to an image. Our test code cowardly refuses to do this.
func (*Desktop) Capture() image.Image {
return nil // could be implemented if required for testing
}

// ContentSizePixels returns a default value for how much space maximised apps should use
func (*Desktop) ContentSizePixels(_ *fynedesk.Screen) (uint32, uint32) {
return uint32(320), uint32(240)
Expand Down
11 changes: 10 additions & 1 deletion test/window.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package test

import "fyne.io/fynedesk"
import (
"image"

"fyne.io/fynedesk"
)

// Window is an in-memory virtual window for test purposes
type Window struct {
Expand All @@ -16,6 +20,11 @@ func NewWindow(title string) *Window {
return win
}

// Capture the contents of the window. Our test code cowardly refuses to do this.
func (w *Window) Capture() image.Image {
return nil // we can add this if required for testing
}

// Close this test window
func (w *Window) Close() {
// no-op
Expand Down
27 changes: 16 additions & 11 deletions window.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package fynedesk

import "fyne.io/fyne"
import (
"image"

"fyne.io/fyne"
)

// Window represents a single managed window within a window manager.
// There may be borders or not depending on configuration.
Expand All @@ -11,16 +15,17 @@ type Window interface {
Maximized() bool // Is the window Maximized?
TopWindow() bool // Is this the window on top?

Close() // Close this window and possibly the application running it
Focus() // Ask this window to get input focus
Fullscreen() // Request to fullscreen this window
Iconify() // Request to iconify this window
Maximize() // Request to resize this window to it's largest possible size
RaiseAbove(Window) // Raise this window above a given other window
RaiseToTop() // Raise this window to the top of the stack
Unfullscreen() // Request to unfullscreen this window
Uniconify() // Request to restore this window and possibly children of this window from being minimized
Unmaximize() // Request to restore this window to its size before being maximized
Capture() image.Image // Capture the contents of this window to an image
Close() // Close this window and possibly the application running it
Focus() // Ask this window to get input focus
Fullscreen() // Request to fullscreen this window
Iconify() // Request to iconify this window
Maximize() // Request to resize this window to it's largest possible size
RaiseAbove(Window) // Raise this window above a given other window
RaiseToTop() // Raise this window to the top of the stack
Unfullscreen() // Request to unfullscreen this window
Uniconify() // Request to restore this window and possibly children of this window from being minimized
Unmaximize() // Request to restore this window to its size before being maximized

Properties() WindowProperties // Request the properties set on this window
}
Expand Down
3 changes: 3 additions & 0 deletions wm.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package fynedesk

import "image"

// WindowManager describes a full window manager which may be loaded as part of the setup.
type WindowManager interface {
Stack
AddStackListener(StackListener)

Blank()
Capture() image.Image // Capture the contents of the whole desktop to an image
Close()
Run()
}
Expand Down

0 comments on commit 4da4414

Please sign in to comment.