Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[widget audit] ImageView #1956

Merged
merged 44 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
402e036
Update ImageView documentation
mhsmith May 30, 2023
043d722
Port ImageView core tests to pytest
mhsmith May 30, 2023
ae5a98a
Add change note
mhsmith May 30, 2023
e1d5c7e
Port tests for base Image.
freakboy3742 Jun 1, 2023
0802c9b
Update API and tests for ImageView.
freakboy3742 Jun 1, 2023
66b9fdd
Prototype cocoa implementation of new resizing behavior.
freakboy3742 Jun 1, 2023
d4d2216
Merge branch 'pack-fixes' into audit-imageview
freakboy3742 Jun 4, 2023
03d7564
Merge remote-tracking branch 'upstream/main' into audit-imageview
freakboy3742 Jun 4, 2023
8c2ffac
Merge branch 'focus-handler-fix' into audit-imageview
freakboy3742 Jun 6, 2023
b10f648
Clarify height/width interpretation when undefined.
freakboy3742 Jun 6, 2023
baca5a3
Refactor handling of paths.
freakboy3742 Jun 6, 2023
fc66474
Merge branch 'app-paths' into audit-imageview
freakboy3742 Jun 7, 2023
131ea35
100% coverage on Cocoa images and image views.
freakboy3742 Jun 7, 2023
16cce2a
100% coverage on iOS.
freakboy3742 Jun 7, 2023
018ad33
Remove support for loading images from URLs.
freakboy3742 Jun 7, 2023
33965e1
Add future annotations to Image.
freakboy3742 Jun 7, 2023
f1b924f
100% coverage for Android.
freakboy3742 Jun 7, 2023
1537aeb
Initial GTK and Winforms implementations.
freakboy3742 Jun 7, 2023
20d79af
Pin Python and Pillow version to something Android can support.
freakboy3742 Jun 8, 2023
65a0258
100% coverage for winforms.
freakboy3742 Jun 8, 2023
f54bd0b
Return the literal aspect ratio, not a boolean.
freakboy3742 Jun 8, 2023
40d9b0a
100% coverage for GTK Imageview.
freakboy3742 Jun 8, 2023
ccad743
Final docs tweaks.
freakboy3742 Jun 8, 2023
6ba9f03
Enable background color tests, and correct iOS, Android and Cocoa.
freakboy3742 Jun 8, 2023
56775f0
Fill out support for saving files.
freakboy3742 Jun 8, 2023
1c71067
Correct GTK API naming.
freakboy3742 Jun 8, 2023
2dffe06
Ensure the file type is honored on Winforms.
freakboy3742 Jun 8, 2023
8f4d5c0
Correct a cleanup problem on Winforms.
freakboy3742 Jun 8, 2023
da9ce1a
Merge branch 'main' into audit-imageview
freakboy3742 Jun 8, 2023
2bc7b12
Merge branch 'main' into audit-imageview
mhsmith Jun 15, 2023
d92960c
Apply suggestions from code review
freakboy3742 Jun 15, 2023
c201ec6
Add error handling for bad images.
freakboy3742 Jun 15, 2023
f03f7b9
Misc documentation cleanups.
freakboy3742 Jun 15, 2023
72a72b1
Make test resilient for Windows paths.
freakboy3742 Jun 16, 2023
4dfefd2
Use a different constructor to avoid iOS memory issues.
freakboy3742 Jun 16, 2023
809607f
More tweaks to avoid segfaults.
freakboy3742 Jun 16, 2023
1fdf609
Add scaling tests to example app
mhsmith Jun 21, 2023
a4b5a3d
Clarify that image format support is for loading and saving.
freakboy3742 Jun 22, 2023
f558ab8
Clarify types and exception docstrings.
freakboy3742 Jun 22, 2023
8a7d0dc
Merge branch 'main' into audit-imageview
freakboy3742 Jun 22, 2023
a5b47d1
Clarify interpretation of image sizing and Pack handling of images.
freakboy3742 Jun 22, 2023
8b582f7
Merge branch 'main' into audit-imageview
freakboy3742 Jun 22, 2023
abfb125
Corrected Pack when a flex child has a minimum size exceeding the fle…
freakboy3742 Jun 23, 2023
c259379
Clarify the use of object-fit:contain.
freakboy3742 Jun 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,10 @@ jobs:
uses: actions/setup-python@v4.6.1
if: ${{ matrix.setup-python }}
with:
# We're not using 3.11 yet, because the testbed's ProxyEventLoop isn't
# compatible with it (https://github.com/beeware/toga/issues/1982).
# We're not using Python 3.11 yet, because:
# * The testbed's ProxyEventLoop has some problems with it
# (https://github.com/beeware/toga/issues/1982).
# * It doesn't have an Android build of Pillow yet.
python-version: "3.10"
- name: Install dependencies
run: |
Expand Down
41 changes: 32 additions & 9 deletions android/src/toga_android/images.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
from .libs.android.graphics import BitmapFactory
from pathlib import Path

from java.io import FileOutputStream

from .libs.android.graphics import Bitmap, BitmapFactory


class Image:
def __init__(self, interface, path=None, url=None, data=None):
def __init__(self, interface, path=None, data=None):
self.interface = interface
self.path = path
self.url = url

if path:
self.native = BitmapFactory.decodeFile(str(path))
elif url:
# Android BitmapFactory nor ImageView provide a convenient async way to fetch images by URL
self.native = None
elif data:
if self.native is None:
raise ValueError(f"Unable to load image from {path}")
else:
self.native = BitmapFactory.decodeByteArray(data, 0, len(data))
if self.native is None:
raise ValueError("Unable to load image from data")

def get_width(self):
return self.native.getWidth()

def get_height(self):
return self.native.getHeight()

def save(self, path):
self.interface.factory.not_implemented("Image.save()")
path = Path(path)
try:
format = {
".jpg": Bitmap.CompressFormat.JPEG,
".jpeg": Bitmap.CompressFormat.JPEG,
".png": Bitmap.CompressFormat.PNG,
}[path.suffix.lower()]
str_path = str(path)
except KeyError:
raise ValueError(f"Don't know how to save image of type {path.suffix!r}")

out = FileOutputStream(str_path)
self.native.compress(format, 90, out)
out.flush()
out.close()
1 change: 1 addition & 0 deletions android/src/toga_android/libs/android/graphics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rubicon.java import JavaClass

Bitmap = JavaClass("android/graphics/Bitmap")
BitmapFactory = JavaClass("android/graphics/BitmapFactory")
Color = JavaClass("android/graphics/Color")
DashPathEffect = JavaClass("android/graphics/DashPathEffect")
Expand Down
28 changes: 27 additions & 1 deletion android/src/toga_android/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
from toga.widgets.imageview import rehint_imageview

from ..libs.android.widget import ImageView as A_ImageView
from .base import Widget


class ImageView(Widget):
def create(self):
self.native = A_ImageView(self._native_activity)
self.native.setAdjustViewBounds(True)

def set_background_color(self, value):
self.set_background_simple(value)

def set_image(self, image):
if image and image._impl.native:
if image:
self.native.setImageBitmap(image._impl.native)
else:
self.native.setImageDrawable(None)

def rehint(self):
# User specified sizes are in "pixels", which is DP;
# we need to convert all sizes into SP.
dpi = self.native.getContext().getResources().getDisplayMetrics().densityDpi
# Toga needs to know how the current DPI compares to the platform default,
# which is 160: https://developer.android.com/training/multiscreen/screendensities
scale = float(dpi) / 160

width, height, aspect_ratio = rehint_imageview(
image=self.interface.image, style=self.interface.style, scale=scale
)
self.interface.intrinsic.width = width
self.interface.intrinsic.height = height
if aspect_ratio is not None:
self.native.setScaleType(A_ImageView.ScaleType.FIT_CENTER)
else:
self.native.setScaleType(A_ImageView.ScaleType.FIT_XY)
14 changes: 14 additions & 0 deletions android/tests_backend/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from android.graphics import Bitmap

from .probe import BaseProbe


class ImageProbe(BaseProbe):
def __init__(self, app, image):
super().__init__()
self.app = app
self.image = image
assert isinstance(self.image._impl.native, Bitmap)

def supports_extension(self, extension):
return extension.lower() in {".jpg", ".jpeg", ".png"}
11 changes: 11 additions & 0 deletions android/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from android.widget import ImageView

from .base import SimpleProbe


class ImageViewProbe(SimpleProbe):
native_class = ImageView

@property
def preserve_aspect_ratio(self):
return self.native.getScaleType() == ImageView.ScaleType.FIT_CENTER
1 change: 1 addition & 0 deletions changes/1956.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ImageView widget now has 100% test coverage, and complete API documentation.
1 change: 1 addition & 0 deletions changes/1956.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Images and ImageViews no longer support loading images from URLs. If you need to display an image from a URL, use a background task to obtain the image data asynchronously, then create the Image and/or set the ImageView ``image`` property on the completion of the asychronous load.
64 changes: 33 additions & 31 deletions cocoa/src/toga_cocoa/images.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from pathlib import Path

from toga_cocoa.libs import (
NSURL,
NSBitmapImageFileType,
NSBitmapImageRep,
NSData,
Expand All @@ -10,45 +9,48 @@


class Image:
def __init__(self, interface, path=None, url=None, data=None):
def __init__(self, interface, path=None, data=None):
self.interface = interface
self.path = path
self.url = url

if path:
self.native = NSImage.alloc().initWithContentsOfFile(str(path))
elif url:
self.native = NSImage.alloc().initByReferencingURL(
NSURL.URLWithString_(url)
)
elif data:
if isinstance(data, NSData):
nsdata = data

try:
# We *should* be able to do a direct NSImage.alloc.init...(),
# but for some reason, this segfaults in some environments
# when loading invalid images. On iOS we can avoid this by
# using the class-level constructors; on macOS we need to
# ensure we have a valid allocated image, then try to init it.
image = NSImage.alloc().retain()
if path:
self.native = image.initWithContentsOfFile(str(path))
if self.native is None:
raise ValueError(f"Unable to load image from {path}")
else:
nsdata = NSData.dataWithBytes(data, length=len(data))
self.native = image.initWithData(nsdata)
if self.native is None:
raise ValueError("Unable to load image from data")
finally:
image.release()

self.native = NSImage.alloc().initWithData(nsdata)
def get_width(self):
return self.native.size.width

def get_height(self):
return self.native.size.height

def save(self, path):
path = Path(path)
try:
if path.suffix == "":
# If no suffix is provided in the filename, default to PNG,
# and append that suffix to the filename.
str_path = str(path) + ".png"
filetype = NSBitmapImageFileType.PNG
else:
filetype = {
".jpg": NSBitmapImageFileType.JPEG,
".jpeg": NSBitmapImageFileType.JPEG,
".png": NSBitmapImageFileType.PNG,
".gif": NSBitmapImageFileType.GIF,
".bmp": NSBitmapImageFileType.BMP,
".tiff": NSBitmapImageFileType.TIFF,
}[path.suffix.lower()]
str_path = str(path)
filetype = {
".jpg": NSBitmapImageFileType.JPEG,
".jpeg": NSBitmapImageFileType.JPEG,
".png": NSBitmapImageFileType.PNG,
".gif": NSBitmapImageFileType.GIF,
".bmp": NSBitmapImageFileType.BMP,
".tiff": NSBitmapImageFileType.TIFF,
}[path.suffix.lower()]
str_path = str(path)
except KeyError:
raise ValueError(f"Don't know how to save image of type {path.suffix}")
raise ValueError(f"Don't know how to save image of type {path.suffix!r}")

bitmapData = NSBitmapImageRep.representationOfImageRepsInArray(
self.native.representations,
Expand Down
2 changes: 1 addition & 1 deletion cocoa/src/toga_cocoa/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def set_enabled(self, value):
# APPLICATOR

def set_bounds(self, x, y, width, height):
# print("SET BOUNDS ON", self.interface, x, y, width, height)
# print(f"SET BOUNDS ON {self.interface} {width}x{height} @ ({x},{y})")
self.constraints.update(x, y, width, height)

def set_alignment(self, alignment):
Expand Down
30 changes: 16 additions & 14 deletions cocoa/src/toga_cocoa/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from toga.widgets.imageview import rehint_imageview
from toga_cocoa.libs import (
NSImage,
NSImageAlignment,
NSImageFrameNone,
NSImageScaleAxesIndependently,
NSImageScaleProportionallyUpOrDown,
NSImageView,
NSSize,
)

from .base import Widget
Expand All @@ -14,26 +14,28 @@ class ImageView(Widget):
def create(self):
self.native = NSImageView.alloc().init()

# self._impl.imageFrameStyle = NSImageFrameGrayBezel
# self.native.imageFrameStyle = NSImageFrameGrayBezel
self.native.imageFrameStyle = NSImageFrameNone
self.native.imageAlignment = NSImageAlignment.Center.value
self.native.imageScaling = NSImageScaleProportionallyUpOrDown
self.native.imageScaling = NSImageScaleAxesIndependently

# Add the layout constraints
self.add_constraints()

def set_image(self, image):
if image:
self.native.image = self.interface._image._impl.native
self.native.image = self.interface.image._impl.native
else:
width = 0
height = 0
if self.interface.style.width:
width = self.interface.style.width
if self.interface.style.height:
height = self.interface.style.height

self.native.image = NSImage.alloc().initWithSize(NSSize(width, height))
self.native.image = None

def rehint(self):
pass
width, height, aspect_ratio = rehint_imageview(
image=self.interface.image,
style=self.interface.style,
)
self.interface.intrinsic.width = width
self.interface.intrinsic.height = height
if aspect_ratio is not None:
self.native.imageScaling = NSImageScaleProportionallyUpOrDown
else:
self.native.imageScaling = NSImageScaleAxesIndependently
14 changes: 14 additions & 0 deletions cocoa/tests_backend/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from toga_cocoa.libs import NSImage

from .probe import BaseProbe


class ImageProbe(BaseProbe):
def __init__(self, app, image):
super().__init__()
self.app = app
self.image = image
assert isinstance(self.image._impl.native, NSImage)

def supports_extension(self, extension):
return extension.lower() in {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"}
11 changes: 11 additions & 0 deletions cocoa/tests_backend/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from toga_cocoa.libs import NSImageScaleProportionallyUpOrDown, NSImageView

from .base import SimpleProbe


class ImageViewProbe(SimpleProbe):
native_class = NSImageView

@property
def preserve_aspect_ratio(self):
return self.native.imageScaling == NSImageScaleProportionallyUpOrDown
Loading