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.
- toga_winforms/__init__.py +106 -0
- toga_winforms/app.py +275 -0
- toga_winforms/colors.py +24 -0
- toga_winforms/command.py +121 -0
- toga_winforms/container.py +70 -0
- toga_winforms/dialogs.py +329 -0
- toga_winforms/factory.py +98 -0
- toga_winforms/fonts.py +123 -0
- toga_winforms/hardware/__init__.py +0 -0
- toga_winforms/icons.py +26 -0
- toga_winforms/images.py +56 -0
- toga_winforms/keys.py +173 -0
- toga_winforms/libs/WebView2/Microsoft.Web.WebView2.Core.dll +0 -0
- toga_winforms/libs/WebView2/Microsoft.Web.WebView2.WinForms.dll +0 -0
- toga_winforms/libs/WebView2/README.md +7 -0
- toga_winforms/libs/WebView2/runtimes/win-arm64/native/WebView2Loader.dll +0 -0
- toga_winforms/libs/__init__.py +0 -0
- toga_winforms/libs/comctl32.py +45 -0
- toga_winforms/libs/extensions.py +37 -0
- toga_winforms/libs/fonts.py +48 -0
- toga_winforms/libs/gdi32.py +22 -0
- toga_winforms/libs/kernel32.py +28 -0
- toga_winforms/libs/proactor.py +165 -0
- toga_winforms/libs/shcore.py +5 -0
- toga_winforms/libs/user32.py +197 -0
- toga_winforms/libs/win32constants.py +182 -0
- toga_winforms/libs/win32misc.py +85 -0
- toga_winforms/libs/win32structures.py +240 -0
- toga_winforms/menus.py +75 -0
- toga_winforms/paths.py +30 -0
- toga_winforms/resources/__init__.py +0 -0
- toga_winforms/resources/runtime.json +15 -0
- toga_winforms/resources/spinner.gif +0 -0
- toga_winforms/resources/toga.ico +0 -0
- toga_winforms/resources/win32.manifest +26 -0
- toga_winforms/screens.py +83 -0
- toga_winforms/statusicons.py +119 -0
- toga_winforms/widgets/__init__.py +0 -0
- toga_winforms/widgets/activityindicator.py +127 -0
- toga_winforms/widgets/base.py +201 -0
- toga_winforms/widgets/box.py +11 -0
- toga_winforms/widgets/button.py +72 -0
- toga_winforms/widgets/canvas.py +474 -0
- toga_winforms/widgets/dateinput.py +49 -0
- toga_winforms/widgets/detailedlist.py +856 -0
- toga_winforms/widgets/divider.py +45 -0
- toga_winforms/widgets/imageview.py +44 -0
- toga_winforms/widgets/label.py +33 -0
- toga_winforms/widgets/mapview.py +226 -0
- toga_winforms/widgets/multilinetextinput.py +147 -0
- toga_winforms/widgets/numberinput.py +74 -0
- toga_winforms/widgets/optioncontainer.py +79 -0
- toga_winforms/widgets/passwordinput.py +7 -0
- toga_winforms/widgets/progressbar.py +84 -0
- toga_winforms/widgets/scrollcontainer.py +134 -0
- toga_winforms/widgets/selection.py +127 -0
- toga_winforms/widgets/slider.py +63 -0
- toga_winforms/widgets/splitcontainer.py +87 -0
- toga_winforms/widgets/switch.py +47 -0
- toga_winforms/widgets/table.py +419 -0
- toga_winforms/widgets/textinput.py +101 -0
- toga_winforms/widgets/timeinput.py +53 -0
- toga_winforms/widgets/tree.py +972 -0
- toga_winforms/widgets/webview.py +311 -0
- toga_winforms/window.py +509 -0
- toga_winforms-0.5.4.dist-info/METADATA +68 -0
- toga_winforms-0.5.4.dist-info/RECORD +72 -0
- toga_winforms-0.5.4.dist-info/WHEEL +5 -0
- toga_winforms-0.5.4.dist-info/entry_points.txt +42 -0
- toga_winforms-0.5.4.dist-info/licenses/LICENSE +27 -0
- toga_winforms-0.5.4.dist-info/licenses/LICENSE.WebView2 +27 -0
- 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()
|
toga_winforms/colors.py
ADDED
|
@@ -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)
|
toga_winforms/command.py
ADDED
|
@@ -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)
|