C# - Capturing CTRL-Mouse wheel in WebBrowser control - c#

I'm developing a Windows Forms application in C# with an embedded WebBrowser control to "dummy-proof" (i.e. disable context menus, back button, free navigation, etc.) access to a third-party web application.
Right now I'm trying to add the Zoom feature to my custom browser. I have the keyboard combos working for it (CTRL + and CTRL - make the correct OLE calls to the underlying ActiveX WebBrowser object), but among the other frustrating things about WebBrowser I've had to deal with, I can't seem to figure out how to capture CTRL-Mouse wheel to simulate the Zoom function like IE does. I've looked everywhere to find a solution to this but to no avail.
To try to figure it out, I created an empty form with just the WebBrowser control in it, and found the following:
CTRL-MouseWheel always fires the MouseWheel event when the parent form has focus and the mouse cursor is hovering over the top of the window (e.g. over the title of the application), or when the mouse cursor is hovering over the WebBrowser control when it does not appear to have focus even though the parent form has focus.
CTRL-MouseWheel never fires the MouseWheel event when the mouse cursor is hovering over the WebBrowser control and WebBrowser has focus, and there seems to be no effect.
Using the mouse wheel without CTRL scrolls the window contents of WebBrowser but does not fire the MouseWheel event until the vertical scroll bar has fully reached either the top or the bottom.
Intercepting the Message for WM_MOUSEWHEEL by overriding WndProc and DefWndProc both for a sample class inherited from WebBrowser and for the parent form applies only for the above conditions (with wParam properly denoting MK_CONTROL).
The PreviewKeyDown event fires when CTRL is pressed, as expected, but still does nothing in unison with the mouse wheel.
So I guess the Message is being processed below the parent form and the managed control level and does not bubble up to where I can intercept or even handle it. Is there a way to do this, or some other way to simulate zooming in and out using CTRL-MouseWheel?
Thanks for reading!

First cast the WebBrowser.Document.DomDocument to the right interface in the mshtml namespace, like mshtml.HTMLDocumentEvents2_Event, then you can handle (and cancel) mousewheel events. I'm not sure, but I think you need to wire up the event handler anytime the document is changed, so I do it on the WebBrowser.DocumentCompleted event. I'm also not sure if you need to release any COM objects.
This was frustrating enough that I got it to work and stopped caring...
Here is at least one document explaining the basics: How to handle document events in a Visual C# .NET application
For your specific case, just conditionally squash the onmousewheel event, based on whether or not the CTRL key is pressed.
private void webBrowser_DocumentCompleted(object sender,
WebBrowserDocumentCompletedEventArgs e)
{
if (webBrowser.Url.ToString() == "about:blank")
return;
var docEvents = (mshtml.HTMLDocumentEvents2_Event)webBrowser.Document.DomDocument;
docEvents.onmousewheel -= docEvents_onmousewheel; //may not be necessary?
docEvents.onmousewheel += docEvents_onmousewheel;
}
bool docEvents_onmousewheel(mshtml.IHTMLEventObj pEvtObj)
{
if (pEvtObj.ctrlKey)
{
pEvtObj.cancelBubble = true; //not sure what this does really
pEvtObj.returnValue = false; //this cancels the event
return false; //not sure what this does really
}
else
return true; //again not sure what this does
}
Now if you need to know the Wheel Delta (scroll amount), you'll want to cast the events object to yet another interface.
bool docEvents_onmousewheel(mshtml.IHTMLEventObj pEvtObj)
{
var wheelEventObj = (mshtml.IHTMLEventObj4)pEvtObj;
var delta = wheelEventObj.wheelDelta;
[...]
}

Perhaps using SetWindowsHookEx to look for these events may work for you. This is what I've used to get scroll wheel events on top of the ActiveX MapPoint control.
Be aware there are some quirks with this on Windows 7 that may require some tinkering. For more details do a search for SetWindowsHookEx on Windows 7 on the MSDN forums.

To solve this problem you have to listen for and handle these messages:
OLECMDID_GETZOOMRANGE
OLECMDID_OPTICAL_GETZOOMRANGE
OLECMDID_OPTICAL_ZOOM
OLECMDID_ZOOM
They're dispatched by Internet Explorer. See the remarks on MSDN.

This is the code I used to disable ctrl+shift: You need to change the behavior of WndProc in the deepest control "Internet Explorer_Server",
Do this after your web browser is ready:
IntPtr wbHandle = Win32.FindWindowEx(this.wbMain.Handle, IntPtr.Zero, "Shell Embedding", String.Empty);
wbHandle = Win32.FindWindowEx(wbHandle, IntPtr.Zero, "Shell DocObject View", String.Empty);
wbHandle = Win32.FindWindowEx(wbHandle, IntPtr.Zero, "Internet Explorer_Server", String.Empty);
WbInternal wb = new WbInternal(wbHandle);
class WbInternal : NativeWindow
{
public WbInternal(IntPtr handle)
{
this.AssignHandle(handle);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MOUSEWHEEL)
{
if (((int)m.WParam & 0x00FF) == MK_SHIFT)
{
return;
}
}
base.WndProc(ref m);
}
}
You can find more message about WM_MOUSEWHEEL from MSDN.
I find this from MSDN. But I forgot the link, Once find, will append it here.

I couldn't get any of these to work reliably so after some (frustrating) experimentation, I came up with a derivative of the answer posted by TCC. My webbrowser control is hosted in a usercontrol. The main differences are I use a class-level variable for the HTMLDocumentEvents2_Event so I can unsubscribe successfully, and I set the mshtml.IHTMLEventObj pEvtObj.Returnvalue to true.. seems to work well now..
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_wbData = (WebBrowser)FindElement("DataBrowser");
_horzScroll = (ScrollBar)FindElement("HorizontalScroll");
_vertScroll = (ScrollBar)FindElement("VerticalScroll");
_wbData.LoadCompleted += new System.Windows.Navigation.LoadCompletedEventHandler(OnLoadCompleted);
_horzScroll.Scroll += new ScrollEventHandler(OnHorizontalScroll);
_vertScroll.Scroll += new ScrollEventHandler(OnVerticalScroll);
LoadDefault();
EnableSoundEffects(SoundEffects);
}
private void OnHorizontalScroll(object sender, ScrollEventArgs e)
{
// _wbData.Handle
mshtml.HTMLDocument htmlDoc = _wbData.Document as mshtml.HTMLDocument;
_horzPosition = (int)e.NewValue;
if (htmlDoc != null && htmlDoc.body != null)
htmlDoc.parentWindow.scroll(_horzPosition, _vertPosition);
}
private void OnVerticalScroll(object sender, ScrollEventArgs e)
{
mshtml.HTMLDocument htmlDoc = _wbData.Document as mshtml.HTMLDocument;
_vertPosition = (int)e.NewValue;
if (htmlDoc != null && htmlDoc.body != null)
htmlDoc.parentWindow.scroll(_horzPosition, _vertPosition);
}
private void OnLoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
mshtml.HTMLDocument htmlDoc = _wbData.Document as mshtml.HTMLDocument;
if (htmlDoc != null && htmlDoc.body != null)
{
mshtml.IHTMLElement2 body = (mshtml.IHTMLElement2)htmlDoc.body;
_horzScroll.Visibility = Visibility.Collapsed;
if (body.scrollHeight > _wbData.ActualHeight)
_vertScroll.Visibility = Visibility.Visible;
else
_vertScroll.Visibility = Visibility.Collapsed;
_vertScroll.ViewportSize = _wbData.ActualHeight;
_vertScroll.Maximum = body.scrollHeight - (_wbData.ActualHeight - 8);
_eventHelper = (HTMLDocumentEvents2_Event)_wbData.Document;
_eventHelper.onmousewheel -= OnMouseWheel;
_eventHelper.onmousewheel += new HTMLDocumentEvents2_onmousewheelEventHandler(OnMouseWheel);
}
}
private bool OnMouseWheel(mshtml.IHTMLEventObj pEvtObj)
{
mshtml.HTMLDocument htmlDoc = _wbData.Document as mshtml.HTMLDocument;
var wheelEventObj = (mshtml.IHTMLEventObj4)pEvtObj;
var delta = wheelEventObj.wheelDelta;
if (htmlDoc != null && htmlDoc.body != null && wheelEventObj != null)
{
_vertPosition += (int)wheelEventObj.wheelDelta;
htmlDoc.parentWindow.scroll(_horzPosition, _vertPosition);
}
pEvtObj.returnValue = true;
return true;
}

Related

Implement form level shortcuts without intercept child control's input

I'm making form level shortcuts with form application.
First, it works with strip menus, however their shortcuts must be combination of any modifier and a word or decimal key. I need only a word or decimal key shortcut. Therefore it does not fit my needs.
And I tried a method that KeyDown and KeyUp events with form's KeyPreview set to true. If I handled the word or decimal keys with Handled and SuppressKeyPress set to true in those events, the child control cannot receive the input even though the control is TextBox or NumericUpDown that is a control that allows text input. Overriding ProcessCmdKey has the same issue.
And then, I tried to check the focusing control actually needs the input by calling focusing control's IsInputKey and IsInputChar. Yes, you cannot call them normally because these methods are protected. I used reflection. But I had no luck. These were not working as I expected.
Finally, I applied a branch that checks the focusing control is TextBox or something allowing input. I had no luck with this either. Because the NumericUpDown's real focus control is a private class called UpDownBase.UpDownEdit. It means that some other controls can have similar mechanism that cannot expect in build time. In short, it can cause bugs.
I think, I could solve this issue if there was a way to catch unhandled input in last key handling phase such like bubbling. But I couldn't find information about it for form applications.
Do I have to override ALL of child controls' ProcessCmdKeys?
Really isn't there a fancy way to solve this problem?
Here is my quick solution. I have changed searching focused control method.
In previous, it searching to the deepest active control. The source was from here. This way caused skipping NumericUpDown case because it is also a container, and returned its private control.
Therefore, I have changed it to searching a control type instead taking the deepest one in active control chain.
static bool IsFocusedControlType<T>(Control control, out T focused)
where T : Control
{
if (control is T t)
{
focused = t;
return true;
}
var container = control as IContainerControl;
while (container != null)
{
control = container.ActiveControl;
if (control is T tt)
{
focused = tt;
return true;
}
container = control as IContainerControl;
}
focused = null;
return false;
}
static bool CanConsumeKey(Form sender, KeyEventArgs e)
{
if (IsFocusedControlType(sender, out NumericUpDown ud))
{
return false;
}
if (IsFocusedControlType(sender, out TextBox tb) && !tb.ReadOnly)
{
return false;
}
if (IsFocusedControlType(sender, out ListView lv) && lv.LabelEdit)
{
return false;
}
if (IsFocusedControlType(sender, out TreeView tv) && tv.LabelEdit)
{
return false;
}
if (IsFocusedControlType(sender, out ComboBox cb) &&
cb.DropDownStyle != ComboBoxStyle.DropDownList)
{
return false;
}
return true;
}
It works fine but I don't think it's fast.

How to route touch events to separate controls UWP C#

I am facing a major problem for custom touch event handling.
The goal is to route the touch event to different controls, depending on how many fingers are used.
For example:
We have a scrollview with many webviews arranged in it.
The zoomfactor is set to present one webview to fullscreen.
Currently I am using the pointer pressed / released functions like this, to configure the controls, so the event will be catched by the right one:
int pointerCount = 0;
private void ScrollView_PointerPressed(object sender, PointerRoutedEventArgs e)
{
try
{
pointerCount++;
if (pointerCount > 3)
pointerCount = 0;
switch (pointerCount)
{
case 1:
// I don't do anything, so it goes to the webview anyway
break;
case 2:
EnableScrolling();
break;
case 3:
ZoomInOut();
break;
default:
return;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
private void EnableScrolling()
{
ScrollViewer.ZoomMode = ZoomMode.Enabled;
ScrollViewer.VerticalScrollMode = ScrollMode.Enabled;
ScrollViewer.HorizontalScrollMode = ScrollMode.Enabled;
}
1-finger events should go to the webview // this works
2-finger events should go to the ScrollView // this is not working, because the webview grabs the touch event and will not release it again
3-finger events should zoom out // this works
The PointerPressed is always called, but the PointerReleased is not called when the PointerPressed was on the webview.
This also results in the effect of not decreasing the pointerCount, so it is possible to do a 1-finger tap 3 times and it results in the 3-finger event and zooms out, that should not happen.
Hopefully you can see the problem and help to resolve it.
If you think this is a way too wrong approach, feel free to show an alternative that works out better.
Well, I couldn't find a proper solution, but I was able to remove the unwanted side effect with upcounting the pointers event if they were released.
private void ScrollViewer_PointerReleased(object sender, PointerRoutedEventArgs e)
{
pointerCount = 0;
}
So the right direction handling is working fine now, but I still can't do something like:
currentWebView.CapturePointer(e.Pointer);
Because it won't root the pointerEvent into it's content, it will call the pointer events of the WebView, but it won't root it into it's html & js content.
I also tried to use
.... wb.ManipulationStarted += Wb_ManipulationStarted; ....
private void Wb_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
// something here
}
but this also will not arrive in the content.
Any ideas for another approach?
I figured out how to handle that, and it is so simple that I could cry...
I just added this line of code to tell the webviews js that there was a event...
await webView.InvokeScriptAsync("eval", new string[] { "document.elementFromPoint(" + pointX + ", " + pointY + ").click();" });
And the webview handles the rest of it. So there is no custom invoke as I was worried about, it's just the standard function invoked, that is always invoked when a touch/click is performed.

How to Capture 'Send' button event for Outlook using UI Automation in C#?

I want to capture 'Send' button event of outlook using UI Automation.
Right now i am able to get 'Focus Change Event' like whenever iam minimizing or maximizing the WINWORD window the the event is raised instead of that i want to get the event on Send button click.
private void SendButtonInvoke()
{
Process[] processes = Process.GetProcessesByName("WINWORD");
AutomationElement aeOutLook = null;
foreach (var item in processes)
{
aeOutLook = AutomationElement.FromHandle(item.MainWindowHandle);
}
//AutomationElement outlookelm = AutomationElement.FromHandle(processName.MainWindowHandle);
AutomationElement buttonAddInstance = aeOutLook.FindFirst(TreeScope.Descendants,
new PropertyCondition(AutomationElement.NameProperty, "Send"));
if (buttonAddInstance == null)
{
MessageBox.Show("Add button instance not found");
}
else
{
AutomationPropertyChangedEventHandler ButtonEvent =
new AutomationPropertyChangedEventHandler(ButtonChecked_EventHandler);
//Attaching the EventHandler
Automation.AddAutomationPropertyChangedEventHandler(buttonAddInstance, TreeScope.Children,
ButtonEvent, AutomationElement.NameProperty);
}
}
private void ButtonChecked_EventHandler(object sender, AutomationEventArgs e)
{
AutomationElement ar = sender as AutomationElement;
MessageBox.Show("Button Clicked Sucessfully.");
}
You have to specifiy the EventHandler for the involved UIA Pattern. (For your case it's likely to be the InvokePattern):
Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, AutomationElement buttonAddInstance ,TreeScope.Element, new AutomationEventHandler(OnStartInvoke));
private static void OnStartInvoke(object src, AutomationEventArgs e)
{
//logic
}
I wrote and tested the code below and it seems to work for me.
private void AddEmailSendEvent()
{
// Find the new email window
PropertyCondition newEmailWindowCondition = new PropertyCondition(AutomationElement.NameProperty, "Untitled - Message (HTML) ");
AutomationElement NewEmailWindow = AutomationElement.RootElement.FindFirst(TreeScope.Children, newEmailWindowCondition);
// Find the Send Button
PropertyCondition sendEmailButtonCondition = new PropertyCondition(AutomationElement.NameProperty, "Send");
AutomationElement sendButton = NewEmailWindow.FindFirst(TreeScope.Descendants, sendEmailButtonCondition);
// If supported, add the invoke event
if (sendButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, sendButton, TreeScope.Element, handler);
}
private void handler(object sender, AutomationEventArgs e)
{
// Do whatever is needed, for testing this just adds a message to my forms Main UI
AddMessage("Invoke event occured");
}
I should note that I'm using the .Net 4.0 automation libs. I've found the older ones don't always work the way I want them. I also tested this with Outlook 2013, and both outlook and the new email message were already open when I tested this. It doesn't handle waiting for them to appear.
Just so your aware, these events don't always work for all controls. Some custom controls are made in such a way the invoke events are not reported to the UI in a way the event can register. With that said, from my testing you should be able to use this method on the send button.
Invoking vs mouse clicks: Just to add a little more detail, the standard control causes the invoke event to fire when a user clicks it. "Invoke" is just the standard event fired on clickable controls. The only time a click wouldn't fire the same invoke is if the developer decided to intercept the click somehow and redirect it elsewhere. I've seen this a lot when people build there own custom controls.
If your not sure about whether a control using/firing the invoke event or not you can get use the Accessible Event Watcher to watch a control as you click it. You can get more information on the tool here: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317979(v=vs.85).aspx

Why doesn't the PropertyGrid raise Keyboard/Mouse events?

Why does this never get called ?
propertyGrid.KeyDown += new KeyEventHandler(propertyGrid_KeyDown);
private void propertyGrid_KeyDown(object sender, KeyEventArgs e)
{
PoorLittleMethod(); //Never gets called
}
This seems to be the same for Mouse event
I'veread on some forums that PGrid is tricky on raising such events as it Inherits them from Control but does not really Raise them. is that true ? If yes, how to bypass that ?
EDIT 1:
As this seems to be "regular", I find it very light from MS not to specify this explicitely on the MSDN Reference of the propertyGrid class and leave events "as is" as if they were usable, whereas they are not. Tricky things like these are at least usually specified in "notes" inside the refs.
EDIT 2:
I am presently coding a workaround. I'll be posting it soon.
The PropertyGrid's KeyDown property is marked as Browsable(false) - presumably the conclusion we can take from this is that it is not supported in an of itself but is in fact present as a side-effect of its inheritance hierarchy.
Though, interestingly enough, its EditorBrowsable attribute (which is also a designer indicator, for Intellisense and the suchlike) is set as EditorBrowsableState.Advanced - where we would expect EditorBrowsableState.Never should the former presumption be true.
Some information from MSDN forums outlines the why of this situation:
From the tool UI Spy we can see the PropertyGrid is a just a panel and it consists of three Windows Controls. Our KeyDown event should be processed by the child control table.
The structure:
-"pane" "PropertyGrid"
--"pane" "Description Pane"
--"table" "Properties Window"
--"tool bar" "ToolBar"
The suggested solution (also provided in the MSDN link) to overcoming this is to use native system calls to retrieve window/control information, subclass NativeWindow and override the WndProc method to handle the events you like, KeyDown in this case.
You can override this from subclass of PropertyGrid to get some key info from windows message
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
CSharp PropertyGrid Events
// Property grid events can’t be easily subscribed to however there is way to get at the KeyUp event without impacting operation.
// Note: The KeyDown event can be subscribed to in the same manner but the propertygrid is NOT updated with the key presses.
// This code is added in hope it may help someone else solve the problem. It is not offered as a total solution.
// First define a class variable to indicate that events have been added.
private bool m_bPropertyGridEventsAdded = false;
public GlassInfoEntryPage(ViewBase view)
: base(view)
{
InitializeComponent();
// Subscribe to SelectedGridItemChanged
m_PropertyGrid.SelectedGridItemChanged += M_PropertyGrid_SelectedGridItemChanged;
}
// Now define a SelectedGridItemChanged Event Handler
private void M_PropertyGrid_SelectedGridItemChanged(object sender, SelectedGridItemChangedEventArgs e)
{
int nXlocation;
int nYlocation;
PropertyGrid oPropertyGrid;
Control oControl;
if (m_bPropertyGridEventsAdded == false)
{
oPropertyGrid = (PropertyGrid)sender;
// Search the Property Grid for a PropertyGridView Control so events can be added to it
for (nXlocation = 0; nXlocation < oPropertyGrid.Width; nXlocation += 10)
{
for (nYlocation = 0; nYlocation < oPropertyGrid.Height; nYlocation += 10)
{
oControl = m_glassInfoPropertyGrid.GetChildAtPoint(new Point(nXlocation, nYlocation));
if (oControl != null)
{
if (oControl.GetType().ToString() == "System.Windows.Forms.PropertyGridInternal.PropertyGridView")
{
// Add Events here
oControl.Controls[1].KeyUp += MyCode_KeyUp;
m_bPropertyGridEventsAdded = true;
break;
}
}
}
if (m_bPropertyGridEventsAdded == true)
{
break;
}
}
}
}
// Handle the events
private void MyCode_KeyUp(object sender, KeyEventArgs e)
{
}

How do I make a Windows Forms control readonly?

Returning to WinForms in VS2008 after a long time.. Tinkering with a OOD problem in VS2008 Express Edition.
I need some controls to be "display only" widgets. The user should not be able to change the value of these controls... the widgets are updated by a periodic update tick event. I vaguely remember there being a ReadOnly property that you could set to have this behavior... can't find it now.
The Enabled property set to false: grays out the control content. I want the control to look normal.
The Locked property set to false: seems to be protecting the user from accidentally distorting the control in the Visual Form Designer.
What am I missing?
For some typical winforms controls:
http://jquiz.wordpress.com/2007/05/29/c-winforms-readonly-controls/
This is also a good tip to preserve the appearance:
Color clr = textBox1.BackColor;
textBox1.ReadOnly = true;
textBox1.BackColor = clr;
To make the forms control Readonly instantly on one click do use the following peice of Code :
public void LockControlValues(System.Windows.Forms.Control Container)
{
try
{
foreach (Control ctrl in Container.Controls)
{
if (ctrl.GetType() == typeof(TextBox))
((TextBox)ctrl).ReadOnly = true;
if (ctrl.GetType() == typeof(ComboBox))
((ComboBox)ctrl).Enabled= false;
if (ctrl.GetType() == typeof(CheckBox))
((CheckBox)ctrl).Enabled = false;
if (ctrl.GetType() == typeof(DateTimePicker))
((DateTimePicker)ctrl).Enabled = false;
if (ctrl.Controls.Count > 0)
LockControlValues(ctrl);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
Then call it from your Button Click Event like this :
LockControlValues(this)
Hope, this helps to solve your problem :
Happy Programming,
Rajan Arora
www.simplyrajan.co.nr
Textbox
.ReadOnly property to true
Controls without ReadOnly
Other control do not have all the time the ReadOnly property. You will require to play with the Events to take off the editing process and keeping your value not editable.
Two relevant properties ReadOnly and Enabled. ReadOnly = true prevents editing grays out the background, but it still allows focus. Enabled = false grays out the background, text and prevents editing or focus.
Windows UI conventions dicate giving the user a visual cue that a control is readonly (that way they won't attempt to edit it and be subsequently frustrated). The grayed out disabled state is the defined system convention, but it's arguable too much of a cue (and not a legibile enough one).
The simplest route is probababy to set your control to ReadOnly, set the background to System.Drawing.SystemColors.Window and then block focus messages. You could do this by catching OnEnter events and immediately moving Focus to another control that's not readonly (say, a Close or Edit button). Or you could derive your own control and eat any WM_SETFOCUS messages. Example below.
I believe various third-party control sets give you additional options and granularity.
public class ReadOnlyTextBox : TextBox
{
const uint WM_SETFOCUS = 0x0007;
public ReadOnlyTextBox()
{
this.ReadOnly = true;
this.BackColor = System.Drawing.SystemColors.Window;
this.ForeColor = System.Drawing.SystemColors.WindowText;
}
protected override void WndProc(ref Message m)
{
// eat all setfocus messages, pass rest to base
if (m.Msg != WM_SETFOCUS)
base.WndProc(ref m);
}
}
I was given this same requirement at work yesterday. Except instead of a textbox I had to make an entire form disabled without changing it's color.
So I replaced a call to
form->Enabled = false;
with
IntPtr hWnd = form->Handle;
HWND window_handle = (HWND)hWnd.ToPointer();
::EnableWindow(window_handle, aEnable ? TRUE:FALSE);
Which worked well. You can see above that I am using managed C++. The entire form is now disabled, but not greyed out.

Categories

Resources