toga-winforms 0.5.4__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. toga_winforms/__init__.py +106 -0
  2. toga_winforms/app.py +275 -0
  3. toga_winforms/colors.py +24 -0
  4. toga_winforms/command.py +121 -0
  5. toga_winforms/container.py +70 -0
  6. toga_winforms/dialogs.py +329 -0
  7. toga_winforms/factory.py +98 -0
  8. toga_winforms/fonts.py +123 -0
  9. toga_winforms/hardware/__init__.py +0 -0
  10. toga_winforms/icons.py +26 -0
  11. toga_winforms/images.py +56 -0
  12. toga_winforms/keys.py +173 -0
  13. toga_winforms/libs/WebView2/Microsoft.Web.WebView2.Core.dll +0 -0
  14. toga_winforms/libs/WebView2/Microsoft.Web.WebView2.WinForms.dll +0 -0
  15. toga_winforms/libs/WebView2/README.md +7 -0
  16. toga_winforms/libs/WebView2/runtimes/win-arm64/native/WebView2Loader.dll +0 -0
  17. toga_winforms/libs/__init__.py +0 -0
  18. toga_winforms/libs/comctl32.py +45 -0
  19. toga_winforms/libs/extensions.py +37 -0
  20. toga_winforms/libs/fonts.py +48 -0
  21. toga_winforms/libs/gdi32.py +22 -0
  22. toga_winforms/libs/kernel32.py +28 -0
  23. toga_winforms/libs/proactor.py +165 -0
  24. toga_winforms/libs/shcore.py +5 -0
  25. toga_winforms/libs/user32.py +197 -0
  26. toga_winforms/libs/win32constants.py +182 -0
  27. toga_winforms/libs/win32misc.py +85 -0
  28. toga_winforms/libs/win32structures.py +240 -0
  29. toga_winforms/menus.py +75 -0
  30. toga_winforms/paths.py +30 -0
  31. toga_winforms/resources/__init__.py +0 -0
  32. toga_winforms/resources/runtime.json +15 -0
  33. toga_winforms/resources/spinner.gif +0 -0
  34. toga_winforms/resources/toga.ico +0 -0
  35. toga_winforms/resources/win32.manifest +26 -0
  36. toga_winforms/screens.py +83 -0
  37. toga_winforms/statusicons.py +119 -0
  38. toga_winforms/widgets/__init__.py +0 -0
  39. toga_winforms/widgets/activityindicator.py +127 -0
  40. toga_winforms/widgets/base.py +201 -0
  41. toga_winforms/widgets/box.py +11 -0
  42. toga_winforms/widgets/button.py +72 -0
  43. toga_winforms/widgets/canvas.py +474 -0
  44. toga_winforms/widgets/dateinput.py +49 -0
  45. toga_winforms/widgets/detailedlist.py +856 -0
  46. toga_winforms/widgets/divider.py +45 -0
  47. toga_winforms/widgets/imageview.py +44 -0
  48. toga_winforms/widgets/label.py +33 -0
  49. toga_winforms/widgets/mapview.py +226 -0
  50. toga_winforms/widgets/multilinetextinput.py +147 -0
  51. toga_winforms/widgets/numberinput.py +74 -0
  52. toga_winforms/widgets/optioncontainer.py +79 -0
  53. toga_winforms/widgets/passwordinput.py +7 -0
  54. toga_winforms/widgets/progressbar.py +84 -0
  55. toga_winforms/widgets/scrollcontainer.py +134 -0
  56. toga_winforms/widgets/selection.py +127 -0
  57. toga_winforms/widgets/slider.py +63 -0
  58. toga_winforms/widgets/splitcontainer.py +87 -0
  59. toga_winforms/widgets/switch.py +47 -0
  60. toga_winforms/widgets/table.py +419 -0
  61. toga_winforms/widgets/textinput.py +101 -0
  62. toga_winforms/widgets/timeinput.py +53 -0
  63. toga_winforms/widgets/tree.py +972 -0
  64. toga_winforms/widgets/webview.py +311 -0
  65. toga_winforms/window.py +509 -0
  66. toga_winforms-0.5.4.dist-info/METADATA +68 -0
  67. toga_winforms-0.5.4.dist-info/RECORD +72 -0
  68. toga_winforms-0.5.4.dist-info/WHEEL +5 -0
  69. toga_winforms-0.5.4.dist-info/entry_points.txt +42 -0
  70. toga_winforms-0.5.4.dist-info/licenses/LICENSE +27 -0
  71. toga_winforms-0.5.4.dist-info/licenses/LICENSE.WebView2 +27 -0
  72. toga_winforms-0.5.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,106 @@
1
+ import os
2
+ import platform
3
+ from pathlib import Path
4
+
5
+ import clr_loader
6
+ from pythonnet import set_runtime
7
+
8
+ try:
9
+ ####################################################################################
10
+ # Toga Winforms requires the use of .NET; either .NET Framework 4.x, or .NET Core.
11
+ #
12
+ # .NET Framework 4.x is available by default on Windows 10 and 11. However, on
13
+ # Windows on ARM64, it is an x86-64 binary, so it can't be used by a native ARM64
14
+ # Python interpreter.
15
+ #
16
+ # However, it *can* be used on ARM64 if you have an x86-64 Python interpreter -
17
+ # which is what you get if you run `py install -3.13` or `py install -3.14`. This
18
+ # will apparently change in Python 3.15.
19
+ #
20
+ # Using .NET Core requires a separate install - but it will be present on a lot of
21
+ # systems.
22
+ #
23
+ # So - try to load .NET Core; if it succeeds, use it. If the load fails, fall back
24
+ # to .NET Framework. If we're on ARM64, check to see if the interpreter is running
25
+ # in emulation mode. If it is, we're OK; if we're not, stop the interpreter; the
26
+ # .NET gives instructions on how to install .NET.
27
+ #
28
+ # But: If TOGA_WINFORMS_USE_NETFX is defined in the environment, ignore .NET Core
29
+ # and prefer .NET Framework 4.x
30
+ ####################################################################################
31
+ if os.environ.get("TOGA_WINFORMS_USE_NETFX", ""): # pragma: no-cover-if-netcore
32
+ raise RuntimeError("Explicitly requesting .NET Framework 4.x")
33
+ else: # pragma: no-cover-if-netfx
34
+ # runtime.json defines the .NET version. .NET 10 is the current LTS release.
35
+ set_runtime(
36
+ clr_loader.get_coreclr(
37
+ runtime_config=Path(__file__).parent / "resources/runtime.json"
38
+ )
39
+ )
40
+
41
+ # .NET Core load succeeded
42
+ _use_dotnet_core = True
43
+ except (clr_loader.util.clr_error.ClrError, RuntimeError): # pragma: no cover
44
+ # .NET Core load failed. This whole branch is no-cover because we can't
45
+ # easily describe no-cover conditions for the failure modes.
46
+ if platform.machine() == "ARM64" and "ARM64" in platform.python_compiler():
47
+ # If you're on a native ARM64 machine running an ARM64 Python, .NET Framework
48
+ # 4.x isn't an option. On Python 3.10 and 3.11, an x86-64 Python running on
49
+ # ARM64 will return `platform.machine() == "AMD64"`, so it fails the first
50
+ # part of the test.
51
+ raise RuntimeError("""
52
+
53
+ On Windows, Toga requires .NET Core 10. Please visit:
54
+
55
+ https://dotnet.microsoft.com/en-us/download/dotnet/10.0
56
+
57
+ and install the .NET Desktop Runtime.""") from None
58
+ else:
59
+ # Either a native x86_64 machine, or an ARM64 machine with and x86_64 Python
60
+ # interpreter in emulation mode. We can use .NET Framework 4.x
61
+ _use_dotnet_core = False
62
+
63
+
64
+ import clr
65
+ import travertino
66
+
67
+ from .libs.user32 import SetProcessDpiAwarenessContext
68
+ from .libs.win32constants import DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
69
+
70
+ # Add a reference to the Winforms assembly
71
+ clr.AddReference("System.Windows.Forms")
72
+
73
+ # .NET Core requires some other explicit assemblies
74
+ if _use_dotnet_core: # pragma: no-cover-if-netfx
75
+ clr.AddReference("Microsoft.Win32.SystemEvents")
76
+ clr.AddReference("System.Windows.Extensions")
77
+ else: # pragma: no-cover-if-netcore
78
+ # We can't do conditional branch coverage, so we need a no-op else
79
+ pass
80
+
81
+ # Add a reference to the WindowsBase assembly. This is needed to access
82
+ # System.Windows.Threading.Dispatcher.
83
+ #
84
+ # This assembly isn't exposed as a simple dot-path name; we have to extract it from the
85
+ # Global Assembly Cache (GAC). The version number and public key doesn't appear to
86
+ # change with Windows version or the underlying .NET, and has been available since
87
+ # Windows 7.
88
+ clr.AddReference(
89
+ "WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
90
+ )
91
+
92
+
93
+ # Enable DPI awareness. This must be done before calling any other UI-related code
94
+ # (https://learn.microsoft.com/en-us/dotnet/desktop/winforms/high-dpi-support-in-windows-forms).
95
+ import System.Windows.Forms as WinForms # noqa: E402
96
+
97
+ WinForms.Application.EnableVisualStyles()
98
+ WinForms.Application.SetCompatibleTextRenderingDefault(False)
99
+
100
+ if SetProcessDpiAwarenessContext is not None:
101
+ if not SetProcessDpiAwarenessContext(
102
+ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
103
+ ): # pragma: no cover
104
+ print("WARNING: Failed to set the DPI Awareness mode for the app.")
105
+
106
+ __version__ = travertino._package_version(__file__, __name__)
toga_winforms/app.py ADDED
@@ -0,0 +1,275 @@
1
+ import asyncio
2
+ import re
3
+ import sys
4
+ import threading
5
+
6
+ import System.Windows.Forms as WinForms
7
+ from Microsoft.Win32 import SystemEvents
8
+ from System import Threading
9
+ from System.Media import SystemSounds
10
+ from System.Net import SecurityProtocolType, ServicePointManager
11
+ from System.Windows.Threading import Dispatcher
12
+
13
+ from toga.dialogs import InfoDialog
14
+ from toga.handlers import WeakrefCallable
15
+
16
+ from .libs.proactor import WinformsProactorEventLoop
17
+ from .screens import Screen as ScreenImpl
18
+
19
+
20
+ def winforms_thread_exception(sender, winforms_exc): # pragma: no cover
21
+ # The PythonException returned by Winforms doesn't give us
22
+ # easy access to the underlying Python stacktrace; so we
23
+ # reconstruct it from the string message.
24
+ # The Python message is helpfully included in square brackets,
25
+ # as the context for the first line in the .net stack trace.
26
+ # So, look for the closing bracket and the start of the Python.net
27
+ # stack trace. Then, reconstruct the line breaks internal to the
28
+ # remaining string.
29
+ print("Traceback (most recent call last):")
30
+ py_exc = winforms_exc.get_Exception()
31
+ full_stack_trace = py_exc.StackTrace
32
+ regex = re.compile(
33
+ r"^\[(?:'(.*?)', )*(?:'(.*?)')\] (?:.*?) Python\.Runtime",
34
+ re.DOTALL | re.UNICODE,
35
+ )
36
+
37
+ def print_stack_trace(stack_trace_line): # pragma: no cover
38
+ for level in stack_trace_line.split("', '"):
39
+ for line in level.split("\\n"):
40
+ if line:
41
+ print(line)
42
+
43
+ stacktrace_relevant_lines = regex.findall(full_stack_trace)
44
+ if len(stacktrace_relevant_lines) == 0:
45
+ print_stack_trace(full_stack_trace)
46
+ else:
47
+ for lines in stacktrace_relevant_lines:
48
+ for line in lines:
49
+ print_stack_trace(line)
50
+
51
+ print(py_exc.Message)
52
+
53
+
54
+ class App:
55
+ # Winforms apps exit when the last window is closed
56
+ CLOSE_ON_LAST_WINDOW = True
57
+ # Winforms apps use default command line handling
58
+ HANDLES_COMMAND_LINE = False
59
+
60
+ def __init__(self, interface):
61
+ self.interface = interface
62
+ self.interface._impl = self
63
+
64
+ # Track whether the app is exiting. This is used to stop the event loop,
65
+ # and shortcut close handling on any open windows when the app exits.
66
+ self._is_exiting = False
67
+ self._exiting_presentation = False
68
+
69
+ # Winforms cursor visibility is a stack; If you call hide N times, you
70
+ # need to call Show N times to make the cursor re-appear. Store a local
71
+ # boolean to allow us to avoid building a deep stack.
72
+ self._cursor_visible = True
73
+
74
+ self.loop = WinformsProactorEventLoop()
75
+ asyncio.set_event_loop(self.loop)
76
+
77
+ def create(self):
78
+ self.native = WinForms.Application
79
+ self.app_context = WinForms.ApplicationContext()
80
+ self.app_dispatcher = Dispatcher.CurrentDispatcher
81
+
82
+ # We would prefer to detect DPI changes directly, using the DpiChanged,
83
+ # DpiChangedBeforeParent or DpiChangedAfterParent events on the window. But none
84
+ # of these events ever fire, possibly because we're missing some app metadata
85
+ # (https://github.com/beeware/toga/pull/2155#issuecomment-2460374101). So
86
+ # instead we need to listen to all events which could cause a DPI change:
87
+ # * DisplaySettingsChanged
88
+ # * Form.LocationChanged and Form.Resize, since a window's DPI is determined
89
+ # by which screen most of its area is on.
90
+ SystemEvents.DisplaySettingsChanged += WeakrefCallable(
91
+ self.winforms_DisplaySettingsChanged
92
+ )
93
+
94
+ # Ensure that TLS1.2 and TLS1.3 are enabled for HTTPS connections.
95
+ # For some reason, some Windows installs have these protocols
96
+ # turned off by default. SSL3, TLS1.0 and TLS1.1 are *not* enabled
97
+ # as they are deprecated protocols and their use should *not* be
98
+ # encouraged.
99
+ try:
100
+ ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12
101
+ except AttributeError: # pragma: no cover
102
+ print(
103
+ "WARNING: Your Windows .NET install does not support TLS1.2. "
104
+ "You may experience difficulties accessing some web server content."
105
+ )
106
+ try:
107
+ ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls13
108
+ except AttributeError: # pragma: no cover
109
+ print(
110
+ "WARNING: Your Windows .NET install does not support TLS1.3. "
111
+ "You may experience difficulties accessing some web server content."
112
+ )
113
+
114
+ # Populate the main window as soon as the event loop is running.
115
+ self.loop.call_soon_threadsafe(self.interface._startup)
116
+
117
+ ######################################################################
118
+ # Native event handlers
119
+ ######################################################################
120
+
121
+ def winforms_DisplaySettingsChanged(self, sender, event):
122
+ # This event is NOT called on the UI thread, so it's not safe for it to access
123
+ # the UI directly.
124
+ self.interface.loop.call_soon_threadsafe(self.update_dpi)
125
+
126
+ def update_dpi(self):
127
+ for window in self.interface.windows:
128
+ window._impl.update_dpi()
129
+
130
+ ######################################################################
131
+ # Commands and menus
132
+ ######################################################################
133
+
134
+ def create_standard_commands(self):
135
+ pass
136
+
137
+ def create_menus(self):
138
+ # Winforms menus are created on the Window.
139
+ for window in self.interface.windows:
140
+ # It's difficult to trigger this on a simple window, because we can't easily
141
+ # modify the set of app-level commands that are registered, and a simple
142
+ # window doesn't exist when the app starts up. Therefore, no-branch the else
143
+ # case.
144
+ if hasattr(window._impl, "create_menus"): # pragma: no branch
145
+ window._impl.create_menus()
146
+
147
+ ######################################################################
148
+ # App lifecycle
149
+ ######################################################################
150
+
151
+ def exit(self): # pragma: no cover
152
+ self._is_exiting = True
153
+ self.native.Exit()
154
+
155
+ def _run_app(self): # pragma: no cover
156
+ # Enable coverage tracing on this non-Python-created thread
157
+ # (https://github.com/nedbat/coveragepy/issues/686).
158
+ if threading._trace_hook:
159
+ sys.settrace(threading._trace_hook)
160
+
161
+ try:
162
+ self.create()
163
+
164
+ # This catches errors in handlers, and prints them
165
+ # in a usable form.
166
+ self.native.ThreadException += WeakrefCallable(winforms_thread_exception)
167
+
168
+ self.loop.run_forever(self)
169
+ except Exception as e:
170
+ # In case of an unhandled error at the level of the app,
171
+ # preserve the Python stacktrace
172
+ self._exception = e
173
+ else:
174
+ # Ensure the event loop is fully closed.
175
+ self.loop.close()
176
+ self._exception = None
177
+
178
+ def main_loop(self):
179
+ thread = Threading.Thread(Threading.ThreadStart(self._run_app))
180
+ thread.SetApartmentState(Threading.ApartmentState.STA)
181
+ thread.Start()
182
+ thread.Join()
183
+
184
+ # If the thread has exited, the _exception attribute will exist.
185
+ # If it's non-None, raise it, as it indicates the underlying
186
+ # app thread had a problem; this is effectibely a re-raise over
187
+ # a thread boundary.
188
+ if self._exception: # pragma: no cover
189
+ raise self._exception
190
+
191
+ def set_icon(self, icon):
192
+ for window in self.interface.windows:
193
+ window._impl.native.Icon = icon._impl.native
194
+
195
+ def set_main_window(self, window):
196
+ pass
197
+
198
+ ######################################################################
199
+ # App resources
200
+ ######################################################################
201
+
202
+ def get_primary_screen(self):
203
+ return ScreenImpl(WinForms.Screen.PrimaryScreen)
204
+
205
+ def get_screens(self):
206
+ primary_screen = self.get_primary_screen()
207
+ screen_list = [primary_screen] + [
208
+ ScreenImpl(native=screen)
209
+ for screen in WinForms.Screen.AllScreens
210
+ if screen != primary_screen.native
211
+ ]
212
+ return screen_list
213
+
214
+ ######################################################################
215
+ # App state
216
+ ######################################################################
217
+
218
+ def get_dark_mode_state(self):
219
+ self.interface.factory.not_implemented("dark mode state")
220
+ return None
221
+
222
+ ######################################################################
223
+ # App capabilities
224
+ ######################################################################
225
+
226
+ def beep(self):
227
+ SystemSounds.Beep.Play()
228
+
229
+ def show_about_dialog(self):
230
+ message_parts = []
231
+ if self.interface.version is not None:
232
+ message_parts.append(
233
+ f"{self.interface.formal_name} v{self.interface.version}"
234
+ )
235
+ else:
236
+ message_parts.append(self.interface.formal_name)
237
+
238
+ if self.interface.author is not None:
239
+ message_parts.append(f"Author: {self.interface.author}")
240
+ if self.interface.description is not None:
241
+ message_parts.append(f"\n{self.interface.description}")
242
+ asyncio.create_task(
243
+ self.interface.dialog(
244
+ InfoDialog(
245
+ f"About {self.interface.formal_name}", "\n".join(message_parts)
246
+ )
247
+ )
248
+ )
249
+
250
+ ######################################################################
251
+ # Cursor control
252
+ ######################################################################
253
+
254
+ def hide_cursor(self):
255
+ if self._cursor_visible:
256
+ WinForms.Cursor.Hide()
257
+ self._cursor_visible = False
258
+
259
+ def show_cursor(self):
260
+ if not self._cursor_visible:
261
+ WinForms.Cursor.Show()
262
+ self._cursor_visible = True
263
+
264
+ ######################################################################
265
+ # Window control
266
+ ######################################################################
267
+
268
+ def get_current_window(self):
269
+ for window in self.interface.windows:
270
+ if WinForms.Form.ActiveForm == window._impl.native:
271
+ return window._impl
272
+ return None
273
+
274
+ def set_current_window(self, window):
275
+ window._impl.native.Activate()
@@ -0,0 +1,24 @@
1
+ from System.Drawing import Color
2
+
3
+ from toga.colors import TRANSPARENT, rgb
4
+
5
+ CACHE = {TRANSPARENT: Color.Transparent}
6
+
7
+
8
+ def native_color(c):
9
+ try:
10
+ color = CACHE[c]
11
+ except KeyError:
12
+ color = Color.FromArgb(
13
+ int(c.rgb.a * 255),
14
+ int(c.rgb.r),
15
+ int(c.rgb.g),
16
+ int(c.rgb.b),
17
+ )
18
+ CACHE[c] = color
19
+
20
+ return color
21
+
22
+
23
+ def toga_color(c):
24
+ return rgb(c.R, c.G, c.B, c.A / 255)
@@ -0,0 +1,121 @@
1
+ import sys
2
+
3
+ from System.ComponentModel import InvalidEnumArgumentException
4
+
5
+ from toga import Command as StandardCommand, Group, Key
6
+ from toga.handlers import WeakrefCallable
7
+ from toga_winforms.keys import toga_to_winforms_key, toga_to_winforms_shortcut
8
+
9
+
10
+ class Command:
11
+ def __init__(self, interface):
12
+ self.interface = interface
13
+ self.native = []
14
+
15
+ @classmethod
16
+ def standard(self, app, id):
17
+ # ---- File menu -----------------------------------
18
+ if id == StandardCommand.NEW:
19
+ return {
20
+ "text": "New",
21
+ "shortcut": Key.MOD_1 + "n",
22
+ "group": Group.FILE,
23
+ "section": 0,
24
+ "order": 0,
25
+ }
26
+ elif id == StandardCommand.OPEN:
27
+ return {
28
+ "text": "Open...",
29
+ "shortcut": Key.MOD_1 + "o",
30
+ "group": Group.FILE,
31
+ "section": 0,
32
+ "order": 10,
33
+ }
34
+ elif id == StandardCommand.SAVE:
35
+ return {
36
+ "text": "Save",
37
+ "shortcut": Key.MOD_1 + "s",
38
+ "group": Group.FILE,
39
+ "section": 0,
40
+ "order": 20,
41
+ }
42
+ elif id == StandardCommand.SAVE_AS:
43
+ return {
44
+ "text": "Save As...",
45
+ "shortcut": Key.MOD_1 + "S",
46
+ "group": Group.FILE,
47
+ "section": 0,
48
+ "order": 21,
49
+ }
50
+ elif id == StandardCommand.SAVE_ALL:
51
+ return {
52
+ "text": "Save All",
53
+ "shortcut": Key.MOD_1 + Key.MOD_2 + "s",
54
+ "group": Group.FILE,
55
+ "section": 0,
56
+ "order": 22,
57
+ }
58
+ elif id == StandardCommand.PREFERENCES:
59
+ # Preferences should be towards the end of the File menu.
60
+ return {
61
+ "text": "Preferences",
62
+ "group": Group.FILE,
63
+ "section": sys.maxsize - 1,
64
+ }
65
+ elif id == StandardCommand.EXIT:
66
+ # Quit should always be the last item, in a section on its own.
67
+ return {
68
+ "text": "Exit",
69
+ "group": Group.FILE,
70
+ "section": sys.maxsize,
71
+ }
72
+ # ---- Help menu -----------------------------------
73
+ elif id == StandardCommand.VISIT_HOMEPAGE:
74
+ return {
75
+ "text": "Visit homepage",
76
+ "enabled": app.home_page is not None,
77
+ "group": Group.HELP,
78
+ }
79
+ elif id == StandardCommand.ABOUT:
80
+ return {
81
+ "text": f"About {app.formal_name}",
82
+ "group": Group.HELP,
83
+ "section": sys.maxsize,
84
+ }
85
+
86
+ raise ValueError(f"Unknown standard command {id!r}")
87
+
88
+ def winforms_Click(self, sender, event):
89
+ return self.interface.action()
90
+
91
+ def set_enabled(self, value):
92
+ if self.native:
93
+ for widget in self.native:
94
+ widget.Enabled = self.interface.enabled
95
+
96
+ def create_menu_item(self, WinformsClass):
97
+ item = WinformsClass(self.interface.text)
98
+
99
+ item.Click += WeakrefCallable(self.winforms_Click)
100
+ if self.interface.shortcut is not None:
101
+ try:
102
+ item.ShortcutKeys = toga_to_winforms_key(self.interface.shortcut)
103
+ # The Winforms key enum is... daft. The "oem" key
104
+ # values render as "Oem" or "Oemcomma", so we need to
105
+ # *manually* set the display text for the key shortcut.
106
+ item.ShortcutKeyDisplayString = toga_to_winforms_shortcut(
107
+ self.interface.shortcut
108
+ )
109
+ except (
110
+ ValueError,
111
+ InvalidEnumArgumentException,
112
+ ) as e: # pragma: no cover
113
+ # Make this a non-fatal warning, because different backends may
114
+ # accept different shortcuts.
115
+ print(f"WARNING: invalid shortcut {self.interface.shortcut!r}: {e}")
116
+
117
+ item.Enabled = self.interface.enabled
118
+
119
+ self.native.append(item)
120
+
121
+ return item
@@ -0,0 +1,70 @@
1
+ import System.Windows.Forms as WinForms
2
+ from System.Drawing import Size
3
+
4
+ from .widgets.base import Scalable
5
+
6
+
7
+ class Container(Scalable):
8
+ def __init__(self, native_parent):
9
+ self.native_parent = native_parent
10
+ self.native_width = self.native_height = 0
11
+ self.content = None
12
+
13
+ self.native_content = WinForms.Panel()
14
+ native_parent.Controls.Add(self.native_content)
15
+
16
+ # See comment in Widget.__init__.
17
+ self.native_content.CreateGraphics().Dispose()
18
+
19
+ @property
20
+ def dpi_scale(self):
21
+ window = self.content.interface.window if self.content else None
22
+ if window:
23
+ return window._impl.dpi_scale
24
+ else:
25
+ return 1
26
+
27
+ @property
28
+ def width(self):
29
+ return self.scale_out(self.native_width)
30
+
31
+ @property
32
+ def height(self):
33
+ return self.scale_out(self.native_height)
34
+
35
+ def set_content(self, widget):
36
+ self.clear_content()
37
+ if widget:
38
+ widget.container = self
39
+ self.content = widget
40
+
41
+ def clear_content(self):
42
+ if self.content:
43
+ self.content.container = None
44
+ self.content = None
45
+
46
+ def resize_content(self, width, height, *, force_refresh=False):
47
+ if (self.native_width, self.native_height) != (width, height):
48
+ self.native_width, self.native_height = (width, height)
49
+ force_refresh = True
50
+
51
+ if force_refresh and self.content:
52
+ self.content.interface.refresh()
53
+
54
+ def refreshed(self):
55
+ layout = self.content.interface.layout
56
+ self.apply_layout(layout.width, layout.height)
57
+
58
+ def apply_layout(self, layout_width, layout_height):
59
+ self.native_content.Size = Size(
60
+ self.scale_in(max(self.width, layout_width)),
61
+ self.scale_in(max(self.height, layout_height)),
62
+ )
63
+
64
+ def add_content(self, widget):
65
+ # The default is to add new controls to the back of the Z-order.
66
+ self.native_content.Controls.Add(widget.native)
67
+ widget.native.BringToFront()
68
+
69
+ def remove_content(self, widget):
70
+ self.native_content.Controls.Remove(widget.native)