Skip to content

Commit 3dc5643

Browse files
authored
Merge 2eab076 into df6bd07
2 parents df6bd07 + 2eab076 commit 3dc5643

4 files changed

Lines changed: 202 additions & 45 deletions

File tree

source/globalCommands.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,8 @@ def script_reportCurrentSelection(self, gesture):
340340
if scriptCount == 0:
341341
speech.speakTextSelected(info.text)
342342
braille.handler.message(selectMessage)
343-
344343
elif scriptCount == 3:
345-
ui.browseableMessage(info.text)
344+
ui.browseableMessage(info.text, copyButton=True, closeButton=True)
346345
return
347346

348347
elif len(info.text) < speech.speech.MAX_LENGTH_FOR_SELECTION_REPORTING:
@@ -2441,6 +2440,8 @@ def _reportFormattingHelper(self, info, browseable=False):
24412440
message,
24422441
# Translators: title for formatting information dialog.
24432442
_("Formatting"),
2443+
copyButton=True,
2444+
closeButton=True,
24442445
)
24452446

24462447
@staticmethod
@@ -4140,7 +4141,7 @@ def script_reportLinkDestination(
41404141
) -> None:
41414142
"""Generates a ui.message or ui.browseableMessage of a link's destination, if focus or caret is
41424143
positioned on a link, or an element with an included link such as a graphic.
4143-
@param forceBrowseable: skips the press once check, and displays the browseableMessage version.
4144+
:param forceBrowseable: skips the press once check, and displays the browseableMessage version.
41444145
"""
41454146
try:
41464147
ti: textInfos.TextInfo = api.getCaretPosition()
@@ -4174,6 +4175,8 @@ def script_reportLinkDestination(
41744175
# Translators: Informs the user that the window contains the destination of the
41754176
# link with given title
41764177
title=_("Destination of: {name}").format(name=obj.name),
4178+
closeButton=True,
4179+
copyButton=True,
41774180
)
41784181
elif presses == 0: # One press
41794182
ui.message(linkDestination) # Speak the link

source/message.html

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,93 @@
44
<TITLE></TITLE>
55
<SCRIPT LANGUAGE=javascript>
66
<!--
7-
function escapeKeyPress(e) {
7+
// Though these should be constants, MSHTML does not support const as of 2024
8+
var args = window.dialogArguments;
9+
var copyStatusTimeout = 5000; // milliseconds before copy status message goes away
10+
var copyEnabled = false;
11+
12+
function handleKeyPress(e) {
13+
/*
14+
This code should be effective for modern browsers (EdgeWebView2),
15+
while also serving in old IE (MSHTML) contexts.
16+
*/
817
e = e || window.event;
9-
if(e.keyCode == 27){ // code for escape
10-
window.close();
18+
var key = e.key || e.keyCode;
19+
if (key === 'Escape' || key === 27) { // code for escape
20+
onCloseButtonPress();
21+
} else if (e.shiftKey && e.ctrlKey && (key === 'C' || key === 67)) { // code for Ctrl+Shift+C
22+
onCopyButtonPress();
23+
}
24+
}
25+
26+
function onCopyButtonPress() {
27+
if (!copyEnabled) return;
28+
// Copy code came from http://www.codestore.net/store.nsf/unid/DOMM-4QHQE8/
29+
var rng = document.body.createTextRange();
30+
rng.moveToElementText(messageDiv);
31+
rng.scrollIntoView();
32+
rng.select();
33+
var success = rng.execCommand("Copy");
34+
rng.collapse(false);
35+
rng.select();
36+
copyStatusDiv.innerHTML = '<p id="copyStatusText" style="font-weight:bold; text-align:center;"></p>';
37+
if (success) { // Notify the user about the copy result
38+
copyStatusText.innerText = args.item('copySuccessfulAlertText');
39+
} else {
40+
copyStatusText.innerText = args.item('copyFailedAlertText');
1141
}
12-
};
42+
// Time out the user alert message
43+
setTimeout(function() {
44+
copyStatusDiv.innerHTML = '';
45+
}, copyStatusTimeout);
46+
}
47+
48+
function onCloseButtonPress() {
49+
window.close();
50+
}
1351

1452
function windowOnLoad() {
15-
var args = window.dialogArguments;
1653
if (args) {
1754
document.title = args.item('title');
18-
messageID.innerHTML = args.item('message');
55+
// We can't use innerText, even for text-only nodes,
56+
// because of #12004.
57+
messageDiv.innerHTML = args.item('message');
58+
// If caller wants a close button
59+
if (args.item('closeButtonText') != null) {
60+
closeButton.innerText = args.item('closeButtonText'); // Assign the (translated) label
61+
closeButton.style.display = 'inline'; // Display the button
62+
buttonDiv.style.display = 'block'; // Display the button section
63+
}
64+
// If caller wants a copy to clip button
65+
if (args.item('copyButtonText') != null) {
66+
copyEnabled = true;
67+
copyButton.innerText = args.item('copyButtonText'); // Assign the (translated) label
68+
copyButtonSpan.style.display = 'inline'; // Display the button
69+
copyStatusDiv.style.display = 'block'; // Display the copy status
70+
buttonDiv.style.display = 'block'; // Display the section
71+
if (args.item('copyButtonAcceleratorAccessibilityLabel') != null) { // If translated text for the accelerator is provided
72+
copyButtonAcceleratorAccessibilityLabel.innerText = args.item('copyButtonAcceleratorAccessibilityLabel');
73+
}
74+
}
75+
} else { // If args wasn't provided, we can do nothing. Just exit
76+
window.close();
1977
}
2078
}
2179
//-->
2280
</SCRIPT>
2381
</HEAD>
24-
<BODY tabindex='0' id='main_body' style="margin:1em" LANGUAGE=javascript onload="return windowOnLoad()" onkeypress="return escapeKeyPress()" >
25-
<div id=messageID></div>
82+
<body tabindex='0' id='main_body' style="margin:1em" onload="return windowOnLoad()" onkeydown="return handleKeyPress()" >
83+
<div id="messageDiv"></div>
84+
85+
<!-- "tabindex" is needed to circumvent an old IE/MSHTML bug in the handling of aria-labelledby.
86+
See: https://www.tpgi.com/aria-labelledby-aria-describedby-support-popular-windows-browsers/ -->
87+
<span id="copyButtonAcceleratorAccessibilityLabel" style="visibility: hidden;" tabindex="-1"></span>
88+
89+
<div id="buttonDiv" style="display: none;"><hr>
90+
<div id="copyStatusDiv" role="status" aria-live="polite"></div>
91+
<span id="copyButtonSpan" style="display: none;">
92+
<button id="copyButton" onclick="onCopyButtonPress();" aria-labelledby="copyButton copyButtonAcceleratorAccessibilityLabel"></button>&nbsp;</span>
93+
<span><button id="closeButton" style="display: none;" onclick="onCloseButtonPress();"></button></span>
94+
</div>
2695
</body>
2796
</html>

source/ui.py

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@
4646
HTMLDLG_VERIFY = 0x0100
4747

4848

49-
def _warnBrowsableMessageNotAvailableOnSecureScreens(title: Optional[str]) -> None:
49+
def _warnBrowsableMessageNotAvailableOnSecureScreens(title: str | None = None) -> None:
5050
"""Warn the user that a browsable message could not be shown on a secure screen (sign-on screen / UAC
5151
prompt).
52-
@param title: If provided, the title of the browsable message to give the user more context.
52+
53+
:param title: If provided, the title of the browsable message to give the user more context.
5354
"""
5455
log.warning(
5556
"While on secure screens browsable messages can not be used."
@@ -72,68 +73,150 @@ def _warnBrowsableMessageNotAvailableOnSecureScreens(title: Optional[str]) -> No
7273
# The title may be something like "Formatting".
7374
"This feature ({title}) is unavailable while on secure screens"
7475
" such as the sign-on screen or UAC prompt.",
75-
)
76-
browsableMessageUnavailableMsg = browsableMessageUnavailableMsg.format(title=title)
76+
).format(title=title)
7777

7878
import wx # Late import to prevent circular dependency.
7979
import gui # Late import to prevent circular dependency.
8080

8181
log.debug("Presenting browsable message unavailable warning.")
82-
gui.messageBox(
82+
wx.CallAfter(
83+
gui.messageBox,
84+
browsableMessageUnavailableMsg,
85+
# Translators: This is the title for a warning dialog, shown if NVDA cannot open a browsable message.
86+
caption=_("Feature unavailable."),
87+
style=wx.ICON_ERROR | wx.OK,
88+
)
89+
90+
91+
def _warnBrowsableMessageComponentFailure(title: str | None = None) -> None:
92+
"""Warn the user that a browsable message could not be shown because of a component failure.
93+
94+
:param title: If provided, the title of the browsable message to give the user more context.
95+
"""
96+
log.warning(
97+
"A browsable message could not be shown because of a component failure."
98+
f" Attempted to open message with title: {title!r}",
99+
)
100+
101+
if not title:
102+
browsableMessageUnavailableMsg: str = _(
103+
# Translators: This is the message for a warning shown if NVDA cannot open a browsable message window
104+
# because of a component failure.
105+
"An error has caused this feature to be unavailable at this time. "
106+
"Restarting NVDA or Windows may solve this problem.",
107+
)
108+
else:
109+
browsableMessageUnavailableMsg: str = _(
110+
# Translators: This is the message for a warning shown if NVDA cannot open a browsable message window
111+
# because of a component failure. This prompt includes the title
112+
# of the Window that could not be opened for context.
113+
# The {title} will be replaced with the title.
114+
# The title may be something like "Formatting".
115+
"An error has caused this feature ({title}) to be unavailable at this time. "
116+
"Restarting NVDA or Windows may solve this problem.",
117+
).format(title=title)
118+
119+
log.debug("Presenting browsable message unavailable warning.")
120+
import wx # Late import to prevent circular dependency.
121+
import gui # Late import to prevent circular dependency.
122+
123+
wx.CallAfter(
124+
gui.messageBox,
83125
browsableMessageUnavailableMsg,
84-
# Translators: This is the title for a warning dialog, shown if NVDA cannot open a browsable message
85-
# dialog.
126+
# Translators: This is the title for a warning dialog, shown if NVDA cannot open a browsable message.
86127
caption=_("Feature unavailable."),
87128
style=wx.ICON_ERROR | wx.OK,
88129
)
89130

90131

91-
def browseableMessage(message: str, title: Optional[str] = None, isHtml: bool = False) -> None:
132+
def browseableMessage(
133+
message: str,
134+
title: str | None = None,
135+
isHtml: bool = False,
136+
closeButton: bool = False,
137+
copyButton: bool = False,
138+
) -> None:
92139
"""Present a message to the user that can be read in browse mode.
93140
The message will be presented in an HTML document.
94-
@param message: The message in either html or text.
95-
@param title: The title for the message.
96-
@param isHtml: Whether the message is html
141+
142+
:param message: The message in either html or text.
143+
:param title: The title for the message, defaults to "NVDA Message".
144+
:param isHtml: Whether the message is html, defaults to False.
145+
:param closeButton: Whether to include a "close" button, defaults to False.
146+
:param copyButton: Whether to include a "copy" (to clipboard) button, defaults to False.
97147
"""
98148
if isRunningOnSecureDesktop():
99-
import wx # Late import to prevent circular dependency.
100-
101-
wx.CallAfter(_warnBrowsableMessageNotAvailableOnSecureScreens, title)
149+
_warnBrowsableMessageNotAvailableOnSecureScreens(title)
102150
return
103151

104152
htmlFileName = os.path.join(globalVars.appDir, "message.html")
105153
if not os.path.isfile(htmlFileName):
154+
_warnBrowsableMessageComponentFailure(title)
106155
raise LookupError(htmlFileName)
156+
107157
moniker = POINTER(IUnknown)()
108-
windll.urlmon.CreateURLMonikerEx(0, htmlFileName, byref(moniker), URL_MK_UNIFORM)
109-
if not title:
110-
# Translators: The title for the dialog used to present general NVDA messages in browse mode.
111-
title = _("NVDA Message")
112-
if not isHtml:
113-
message = f"<pre>{escape(message)}</pre>"
158+
try:
159+
windll.urlmon.CreateURLMonikerEx(0, htmlFileName, byref(moniker), URL_MK_UNIFORM)
160+
except OSError as e:
161+
log.error(f"OS error during URL moniker creation: {e}")
162+
_warnBrowsableMessageComponentFailure(title)
163+
return
164+
except Exception as e:
165+
log.error(f"Unexpected error during URL moniker creation: {e}")
166+
_warnBrowsableMessageComponentFailure(title)
167+
return
168+
114169
try:
115170
d = comtypes.client.CreateObject("Scripting.Dictionary")
116171
except (COMError, OSError):
117172
log.error("Scripting.Dictionary component unavailable", exc_info=True)
118-
# Store the module level message function in a new variable since it is masked by a local variable with
119-
# the same name
120-
messageFunction = globals()["message"]
121-
# Translators: reported when unable to display a browsable message.
122-
messageFunction(_("Unable to display browseable message"))
173+
_warnBrowsableMessageComponentFailure(title)
123174
return
175+
176+
if title is None:
177+
# Translators: The title for the dialog used to present general NVDA messages in browse mode.
178+
title = _("NVDA Message")
124179
d.add("title", title)
180+
181+
if not isHtml:
182+
message = f"<pre>{escape(message)}</pre>"
183+
else:
184+
log.warning("Passing raw HTML to ui.browseableMessage!")
125185
d.add("message", message)
186+
187+
# Translators: A notice to the user that a copy operation succeeded.
188+
d.add("copySuccessfulAlertText", _("Text copied."))
189+
# Translators: A notice to the user that a copy operation failed.
190+
d.add("copyFailedAlertText", _("Couldn't copy to clipboard."))
191+
if closeButton:
192+
# Translators: The text of a button which closes the window.
193+
d.add("closeButtonText", _("Close"))
194+
if copyButton:
195+
# Translators: The label of a button to copy the text of the window to the clipboard.
196+
d.add("copyButtonText", _("Copy"))
197+
# Translators: A portion of an accessibility label for the "Copy" button,
198+
# describing the key to press to activate the button. Currently, this key may only be Ctrl+Shift+C.
199+
# Translation makes sense here if the Control or Shift keys are called something else in a
200+
# given language; or to set this to the empty string if that key combination is unavailable on some keyboard.
201+
d.add("copyButtonAcceleratorAccessibilityLabel", _("Ctrl+Shift+C"))
202+
126203
dialogArgsVar = automation.VARIANT(d)
127204
gui.mainFrame.prePopup()
128-
windll.mshtml.ShowHTMLDialogEx(
129-
gui.mainFrame.Handle,
130-
moniker,
131-
HTMLDLG_MODELESS,
132-
byref(dialogArgsVar),
133-
DIALOG_OPTIONS,
134-
None,
135-
)
136-
gui.mainFrame.postPopup()
205+
try:
206+
windll.mshtml.ShowHTMLDialogEx(
207+
gui.mainFrame.Handle,
208+
moniker,
209+
HTMLDLG_MODELESS,
210+
byref(dialogArgsVar),
211+
DIALOG_OPTIONS,
212+
None,
213+
)
214+
except Exception as e:
215+
log.error(f"Failed to show HTML dialog: {e}")
216+
_warnBrowsableMessageComponentFailure(title)
217+
return
218+
finally:
219+
gui.mainFrame.postPopup()
137220

138221

139222
def message(

user_docs/en/changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The available options are:
2424
* eSpeak NG has been updated to 1.52-dev commit `961454ff`. (#16775)
2525
* Added new languages Faroese and Xextan.
2626
* When using a multi-line braille display via the standard HID braille driver, all lines of cells will be used. (#16993, @alexmoon)
27+
* The Report link destination, Character formatting information, and Speak selection dialogs, now include "Close" and "Copy" buttons for user convenience. (#17018, @XLTechie)
2728

2829
### Bug Fixes
2930

@@ -61,6 +62,7 @@ Please refer to [the developer guide](https://www.nvaccess.org/files/nvda/docume
6162
* It is now possible to redirect objects retrieved from on-screen coordinates, by using the `NVDAObject.objectFromPointRedirect` method. (#16788, @Emil-18)
6263
* Running SCons with the parameter `--all-cores` will automatically pick the maximum number of available CPU cores. (#16943, #16868, @LeonarddeR)
6364
* Developer info now includes information on app architecture (such as AMD64) for the navigator object. (#16488, @josephsl)
65+
* `ui.browseableMessage` may now be called with options to present a button for copying to clipboard, and/or a button for closing the window. (#17018, @XLTechie)
6466

6567
#### Deprecations
6668

0 commit comments

Comments
 (0)