Skip to content

Commit

Permalink
Merge pull request #1930 from proneon267/patch-11
Browse files Browse the repository at this point in the history
Added APIs for detecting multiple displays and setting windows on them.
  • Loading branch information
freakboy3742 authored Feb 11, 2024
2 parents 5d11412 + b025d21 commit caa86a3
Show file tree
Hide file tree
Showing 54 changed files with 1,076 additions and 40 deletions.
8 changes: 8 additions & 0 deletions android/src/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import warnings

from android.content import Context
from android.graphics.drawable import BitmapDrawable
from android.media import RingtoneManager
from android.view import Menu, MenuItem
Expand All @@ -11,6 +12,7 @@
from toga.command import Command, Group, Separator

from .libs import events
from .screens import Screen as ScreenImpl
from .window import Window


Expand Down Expand Up @@ -325,3 +327,9 @@ def hide_cursor(self):

def show_cursor(self):
pass

def get_screens(self):
context = self.native.getApplicationContext()
display_manager = context.getSystemService(Context.DISPLAY_SERVICE)
screen_list = display_manager.getDisplays()
return [ScreenImpl(self, screen) for screen in screen_list]
48 changes: 48 additions & 0 deletions android/src/toga_android/screens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from android.graphics import (
Bitmap,
Canvas as A_Canvas,
)

from toga.screens import Screen as ScreenInterface

from .widgets.base import Scalable


class Screen(Scalable):
_instances = {}

def __new__(cls, app, native):
if native in cls._instances:
return cls._instances[native]
else:
instance = super().__new__(cls)
instance.interface = ScreenInterface(_impl=instance)
instance.native = native
cls._instances[native] = instance
cls.app = app
instance.init_scale(instance.app.native)
return instance

def get_name(self):
return self.native.getName()

def get_origin(self):
return (0, 0)

def get_size(self):
return (
self.scale_out(self.native.getWidth()),
self.scale_out(self.native.getHeight()),
)

def get_image_data(self):
# Get the root view of the current activity
root_view = self.app.native.getWindow().getDecorView().getRootView()
bitmap = Bitmap.createBitmap(
*map(self.scale_in, self.get_size()),
Bitmap.Config.ARGB_8888,
)
canvas = A_Canvas(bitmap)
root_view.draw(canvas)

return bitmap
7 changes: 7 additions & 0 deletions android/src/toga_android/window.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from decimal import ROUND_UP

from android import R
from android.content import Context
from android.graphics import (
Bitmap,
Canvas as A_Canvas,
Expand All @@ -10,6 +11,7 @@
from java.io import ByteArrayOutputStream

from .container import Container
from .screens import Screen as ScreenImpl


class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)):
Expand Down Expand Up @@ -103,6 +105,11 @@ def close(self):
def set_full_screen(self, is_full_screen):
self.interface.factory.not_implemented("Window.set_full_screen()")

def get_current_screen(self):
context = self.app.native.getApplicationContext()
window_manager = context.getSystemService(Context.WINDOW_SERVICE)
return ScreenImpl(self.app, window_manager.getDefaultDisplay())

def get_image_data(self):
bitmap = Bitmap.createBitmap(
self.native_content.getWidth(),
Expand Down
2 changes: 1 addition & 1 deletion android/tests_backend/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async def redraw(self, message=None, delay=0):
print("Waiting for redraw" if message is None else message)
await asyncio.sleep(delay)

def assert_image_size(self, image_size, size):
def assert_image_size(self, image_size, size, screen):
# Sizes are approximate because of scaling inconsistencies.
assert image_size == (
approx(size[0] * self.scale_factor, abs=2),
Expand Down
21 changes: 21 additions & 0 deletions android/tests_backend/screens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from android.view import Display

import toga
from toga.images import Image as TogaImage
from toga_android.widgets.base import Scalable

from .probe import BaseProbe


class ScreenProbe(BaseProbe, Scalable):
def __init__(self, screen):
app = toga.App.app
super().__init__(app)
self.screen = screen
self._impl = screen._impl
self.native = screen._impl.native
self.init_scale(app._impl.native)
assert isinstance(self.native, Display)

def get_screenshot(self, format=TogaImage):
return self.screen.as_image(format=format)
1 change: 1 addition & 0 deletions changes/1930.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Toga apps can now access details about the screens attached to the computer. Window position APIs have been extended to allow for placement on a specific screen, and positioning relative to a specific screen.
1 change: 1 addition & 0 deletions changes/1930.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The macOS implementations of ``Window.as_image()`` and ``Canvas.as_image()`` APIs now return images in native device resolution, not CSS pixel resolution. This will result in images that are double the previous size on Retina displays.
4 changes: 4 additions & 0 deletions cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
objc_method,
objc_property,
)
from .screens import Screen as ScreenImpl
from .window import Window


Expand Down Expand Up @@ -402,6 +403,9 @@ def _submenu(self, group, menubar):
def main_loop(self):
self.loop.run_forever(lifecycle=CocoaLifecycle(self.native))

def get_screens(self):
return [ScreenImpl(native=screen) for screen in NSScreen.screens]

def set_main_window(self, window):
pass

Expand Down
23 changes: 23 additions & 0 deletions cocoa/src/toga_cocoa/libs/core_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ class CGEventRef(c_void_p):

######################################################################
# CGImage.h

CGImageRef = c_void_p
register_preferred_encoding(b"^{CGImage=}", CGImageRef)

core_graphics.CGImageGetWidth.argtypes = [CGImageRef]
core_graphics.CGImageGetWidth.restype = c_size_t

core_graphics.CGImageGetHeight.argtypes = [CGImageRef]
core_graphics.CGImageGetHeight.restype = c_size_t

kCGImageAlphaNone = 0
kCGImageAlphaPremultipliedLast = 1
kCGImageAlphaPremultipliedFirst = 2
Expand All @@ -220,3 +230,16 @@ class CGEventRef(c_void_p):
kCGBitmapByteOrder32Little = 2 << 12
kCGBitmapByteOrder16Big = 3 << 12
kCGBitmapByteOrder32Big = 4 << 12

######################################################################
# CGDirectDisplay.h

CGDirectDisplayID = c_uint32

# CGDirectDisplayID CGMainDisplayID(void);
core_graphics.CGMainDisplayID.restype = CGDirectDisplayID
core_graphics.CGMainDisplayID.argtypes = None

# CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect);
core_graphics.CGDisplayCreateImage.restype = CGImageRef
core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect]
53 changes: 53 additions & 0 deletions cocoa/src/toga_cocoa/screens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from rubicon.objc import CGSize

from toga.screens import Screen as ScreenInterface
from toga_cocoa.libs import (
NSImage,
core_graphics,
)


class Screen:
_instances = {}

def __new__(cls, native):
if native in cls._instances:
return cls._instances[native]
else:
instance = super().__new__(cls)
instance.interface = ScreenInterface(_impl=instance)
instance.native = native
cls._instances[native] = instance
return instance

def get_name(self):
return str(self.native.localizedName)

def get_origin(self):
frame_native = self.native.frame
return (int(frame_native.origin.x), int(frame_native.origin.y))

def get_size(self):
frame_native = self.native.frame
return (int(frame_native.size.width), int(frame_native.size.height))

def get_image_data(self):
# Retrieve the device description dictionary for the NSScreen
device_description = self.native.deviceDescription
# Extract the CGDirectDisplayID from the device description
cg_direct_display_id = device_description.objectForKey_(
"NSScreenNumber"
).unsignedIntValue

cg_image = core_graphics.CGDisplayCreateImage(
cg_direct_display_id,
self.native.frame,
)
# Get the size of the CGImage
target_size = CGSize(
core_graphics.CGImageGetWidth(cg_image),
core_graphics.CGImageGetHeight(cg_image),
)
# Create an NSImage from the CGImage
ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size)
return ns_image
20 changes: 11 additions & 9 deletions cocoa/src/toga_cocoa/widgets/canvas.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
from math import ceil

from rubicon.objc import objc_method, objc_property
from rubicon.objc import CGSize, objc_method, objc_property
from travertino.size import at_least

from toga.colors import BLACK, TRANSPARENT, color
from toga.widgets.canvas import Baseline, FillRule
from toga_cocoa.colors import native_color
from toga_cocoa.images import nsdata_to_bytes
from toga_cocoa.libs import (
CGFloat,
CGPathDrawingMode,
CGRectMake,
NSAttributedString,
NSBitmapImageFileType,
NSFontAttributeName,
NSForegroundColorAttributeName,
NSGraphicsContext,
NSImage,
NSMutableDictionary,
NSPoint,
NSRect,
Expand Down Expand Up @@ -321,16 +320,19 @@ def write_text(self, text, x, y, font, baseline, **kwargs):
)

def get_image_data(self):

bitmap = self.native.bitmapImageRepForCachingDisplayInRect(self.native.bounds)
bitmap.setSize(self.native.bounds.size)
self.native.cacheDisplayInRect(self.native.bounds, toBitmapImageRep=bitmap)

return nsdata_to_bytes(
bitmap.representationUsingType(
NSBitmapImageFileType.PNG,
properties=None,
)
# Get a reference to the CGImage from the bitmap
cg_image = bitmap.CGImage

target_size = CGSize(
core_graphics.CGImageGetWidth(cg_image),
core_graphics.CGImageGetHeight(cg_image),
)
ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size)
return ns_image

# Rehint
def rehint(self):
Expand Down
25 changes: 18 additions & 7 deletions cocoa/src/toga_cocoa/window.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from rubicon.objc import CGSize

from toga.command import Command, Separator
from toga_cocoa.container import Container
from toga_cocoa.images import nsdata_to_bytes
from toga_cocoa.libs import (
SEL,
NSBackingStoreBuffered,
NSBitmapImageFileType,
NSImage,
NSMakeRect,
NSMutableArray,
NSPoint,
Expand All @@ -14,10 +15,13 @@
NSToolbarItem,
NSWindow,
NSWindowStyleMask,
core_graphics,
objc_method,
objc_property,
)

from .screens import Screen as ScreenImpl


def toolbar_identifier(cmd):
return f"Toolbar-{type(cmd).__name__}-{id(cmd)}"
Expand Down Expand Up @@ -305,16 +309,23 @@ def cocoa_windowShouldClose(self):
def close(self):
self.native.close()

def get_current_screen(self):
return ScreenImpl(self.native.screen)

def get_image_data(self):
bitmap = self.container.native.bitmapImageRepForCachingDisplayInRect(
self.container.native.bounds
)
bitmap.setSize(self.container.native.bounds.size)
self.container.native.cacheDisplayInRect(
self.container.native.bounds, toBitmapImageRep=bitmap
)
data = bitmap.representationUsingType(
NSBitmapImageFileType.PNG,
properties=None,

# Get a reference to the CGImage from the bitmap
cg_image = bitmap.CGImage

target_size = CGSize(
core_graphics.CGImageGetWidth(cg_image),
core_graphics.CGImageGetHeight(cg_image),
)
return nsdata_to_bytes(data)
ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size)
return ns_image
11 changes: 6 additions & 5 deletions cocoa/tests_backend/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rubicon.objc import SEL, NSArray, NSObject, ObjCClass, objc_method
from rubicon.objc.api import NSString

import toga
from toga_cocoa.libs.appkit import appkit

NSRunLoop = ObjCClass("NSRunLoop")
Expand Down Expand Up @@ -48,7 +49,7 @@ async def post_event(self, event, delay=None):

async def redraw(self, message=None, delay=None):
"""Request a redraw of the app, waiting until that redraw has completed."""
if self.app.run_slow:
if toga.App.app.run_slow:
# If we're running slow, wait for a second
print("Waiting for redraw" if message is None else message)
delay = 1
Expand All @@ -60,7 +61,7 @@ async def redraw(self, message=None, delay=None):
# for at least one iteration. `runUntilDate:None` does this.
NSRunLoop.currentRunLoop.runUntilDate(None)

def assert_image_size(self, image_size, size):
# Cocoa reports image sizing in the natural screen coordinates, not the size of
# the backing store.
assert image_size == size
def assert_image_size(self, image_size, size, screen):
# Screenshots are captured in native device resolution, not in CSS pixels.
scale = int(screen._impl.native.backingScaleFactor)
assert image_size == (size[0] * scale, size[1] * scale)
16 changes: 16 additions & 0 deletions cocoa/tests_backend/screens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from toga.images import Image as TogaImage
from toga_cocoa.libs import NSScreen

from .probe import BaseProbe


class ScreenProbe(BaseProbe):
def __init__(self, screen):
super().__init__()
self.screen = screen
self._impl = screen._impl
self.native = screen._impl.native
assert isinstance(self.native, NSScreen)

def get_screenshot(self, format=TogaImage):
return self.screen.as_image(format=format)
Loading

0 comments on commit caa86a3

Please sign in to comment.