From abdb70f6919549a25eab456417ce4201875d8a3d Mon Sep 17 00:00:00 2001 From: haru-s Date: Sat, 24 Dec 2022 10:58:18 +0900 Subject: [PATCH] FIX: TextBox is not accepted RETURN and TAB correctly in form that has default button. - General In order to resolve this, keyboard input process was changed similar to WinForms. A part of role of FormMessageFilter class was moved to each control's wndProc(), because TextBox class is must to know that parent form has default button and not. And also remove to hack of ControlStyles enum. - Others Add TextBox example code. Separate TextBoxBase class from textbox.d to textboxbase.d file. Separate SharedControl class from control.d to sharedcontrol.d file. Fix some old alias sytax. Add vscode workspace file. --- dfl.code-workspace | 8 + examples/textbox/.gitignore | 16 + examples/textbox/dub.json | 16 + examples/textbox/dub.selections.json | 7 + examples/textbox/shell.bat | 3 + examples/textbox/source/textbox_sample.d | 127 +++ source/dfl/application.d | 2 +- source/dfl/control.d | 436 ++++---- source/dfl/form.d | 241 ++++- source/dfl/package.d | 4 +- source/dfl/richtextbox.d | 25 +- source/dfl/sharedcontrol.d | 152 +++ source/dfl/textbox.d | 1210 ++++----------------- source/dfl/textboxbase.d | 1214 ++++++++++++++++++++++ 14 files changed, 2146 insertions(+), 1315 deletions(-) create mode 100644 dfl.code-workspace create mode 100644 examples/textbox/.gitignore create mode 100644 examples/textbox/dub.json create mode 100644 examples/textbox/dub.selections.json create mode 100644 examples/textbox/shell.bat create mode 100644 examples/textbox/source/textbox_sample.d create mode 100644 source/dfl/sharedcontrol.d create mode 100644 source/dfl/textboxbase.d diff --git a/dfl.code-workspace b/dfl.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/dfl.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/examples/textbox/.gitignore b/examples/textbox/.gitignore new file mode 100644 index 0000000..60a5f2a --- /dev/null +++ b/examples/textbox/.gitignore @@ -0,0 +1,16 @@ +.dub +docs.json +__dummy.html +docs/ +/hello_dfl +hello_dfl.so +hello_dfl.dylib +hello_dfl.dll +hello_dfl.a +hello_dfl.lib +hello_dfl-test-* +*.exe +*.pdb +*.o +*.obj +*.lst diff --git a/examples/textbox/dub.json b/examples/textbox/dub.json new file mode 100644 index 0000000..e629f60 --- /dev/null +++ b/examples/textbox/dub.json @@ -0,0 +1,16 @@ +{ + "authors": ["haru-s"], + "copyright": "Copyright (C) 2022 haru-s", + "description": "DFL sample code.", + "name": "textbox_sample", + "targetType": "executable", + "targetPath": "bin", + "dependencies": { + "dfl": { + "path": "../../../dfl" + } + }, + "lflags-windows-x86_omf-dmd": ["/exet:nt/su:windows:6.0"], + "lflags-windows-x86_mscoff-dmd": ["/SUBSYSTEM:WINDOWS", "/ENTRY:mainCRTStartup"], + "lflags-windows-x86_64-dmd": ["/SUBSYSTEM:WINDOWS", "/ENTRY:mainCRTStartup"] +} \ No newline at end of file diff --git a/examples/textbox/dub.selections.json b/examples/textbox/dub.selections.json new file mode 100644 index 0000000..203d1ff --- /dev/null +++ b/examples/textbox/dub.selections.json @@ -0,0 +1,7 @@ +{ + "fileVersion": 1, + "versions": { + "dfl": {"path":"../.."}, + "undead": "1.1.8" + } +} diff --git a/examples/textbox/shell.bat b/examples/textbox/shell.bat new file mode 100644 index 0000000..49ab56b --- /dev/null +++ b/examples/textbox/shell.bat @@ -0,0 +1,3 @@ +set dmd_path=c:\d\dmd2\windows +set dmc_path=c:\dmc\dm +cmd diff --git a/examples/textbox/source/textbox_sample.d b/examples/textbox/source/textbox_sample.d new file mode 100644 index 0000000..4f84a03 --- /dev/null +++ b/examples/textbox/source/textbox_sample.d @@ -0,0 +1,127 @@ +import dfl; + +version(Have_dfl) // For DUB. +{ +} +else +{ + pragma(lib, "dfl.lib"); +} + +class MainForm : Form +{ + private TextBox _textbox1; + private TextBox _textbox2; + private TextBox _textbox3; + private TextBox _textbox4; + private TextBox _textbox5; + private Button _button1; + private Button _button2; + + this() + { + // Form setting + text = "TextBox sample"; + size = Size(500,400); + resizeRedraw = true; + + // Button setting (Default button) + _button1 = new Button(); + _button1.location = Point(10,10); + _button1.text = "OK"; + _button1.click ~= (Control c, EventArgs e) + { + this.text = this.text ~ "+"; + }; + _button1.parent = this; + + // Button setting (Default button's enable/disable is switched). + _button2 = new Button(); + _button2.location = Point(100,10); + _button2.text = "On/Off"; + _button2.click ~= (Control c, EventArgs e) + { + if(acceptButton) + { + acceptButton = null; // Default button is now none. + _button1.notifyDefault(false); + _textbox1.text = "Now Off"; + } + else + { + acceptButton = _button1; // Set default button. + _button1.notifyDefault(true); + _textbox1.text = "Now On"; + } + }; + _button2.parent = this; + + // TextBox setting (Single line) + _textbox1 = new TextBox(); + _textbox1.location = Point(200,10); + _textbox1.size = Size(100,30); + _textbox1.multiline = false; // false: One line textbox. + _textbox1.scrollBars = ScrollBars.NONE; // NONE: Without scroll bar. + _textbox1.acceptsReturn = false; // false: Disables RETURN key. + _textbox1.acceptsTab = false; // false: Disables TAB key. + // true: Enables TAB key. But be changed focus only + // because one line textbox is not able to input TAB char. + _textbox1.wordWrap = false; // false: Do not send words that span the right edge to the next line. + _button1.notifyDefault(false); // false: Do not set as default button + _textbox1.text = "Default Off"; + _textbox1.parent = this; + + // TextBox setting (return:yes, tab:yes) + _textbox2 = new TextBox(); + _textbox2.location = Point(10,60); + _textbox2.size = Size(450,50); + _textbox2.multiline = true; // true: Multi line textbox. + _textbox2.scrollBars = ScrollBars.VERTICAL; // VERTICAL: within vertical scroll bar. + _textbox2.acceptsReturn = true; // true: Enables RETURN key. + _textbox2.acceptsTab = true; // true: Enables TAB key. + _textbox2.wordWrap = true; // true: Send words that span the right edge to the next line. + _textbox2.text = "return:yes, tab:yes"; + _textbox2.parent = this; + + // TextBox setting (return:yes, tab:no) + _textbox3 = new TextBox(); + _textbox3.location = Point(10,130); + _textbox3.size = Size(450,50); + _textbox3.multiline = true; // false: One line textbox. + _textbox3.scrollBars = ScrollBars.VERTICAL; // VERTICAL: within vertical scroll bar. + _textbox3.acceptsReturn = true; // true: Enables RETURN key. + _textbox3.acceptsTab = false; // false: Disables TAB key. + _textbox3.wordWrap = true; // true: Send words that span the right edge to the next line. + _textbox3.text = "return:yes, tab:no"; + _textbox3.parent = this; + + // TextBox setting (return:no, tab:yes) + _textbox4 = new TextBox(); + _textbox4.location = Point(10,190); + _textbox4.size = Size(450,50); + _textbox4.multiline = true; // true: Multi line textbox. + _textbox4.scrollBars = ScrollBars.VERTICAL; // VERTICAL: within vertical scroll bar. + _textbox4.acceptsReturn = false; // false: Disables RETURN key. + _textbox4.acceptsTab = true; // true: Enables TAB key. + _textbox4.wordWrap = true; // true: Send words that span the right edge to the next line. + _textbox4.text = "return:no, tab:yes"; + _textbox4.parent = this; + + // TextBox setting (return:no, tab:no) + _textbox5 = new TextBox(); + _textbox5.location = Point(10,250); + _textbox5.size = Size(450,50); + _textbox5.multiline = true; // true: Multi line textbox. + _textbox5.scrollBars = ScrollBars.VERTICAL; // VERTICAL: within vertical scroll bar. + _textbox5.acceptsReturn = false; // false: Disables RETURN key. + _textbox5.acceptsTab = false; // false: Disables TAB key. + _textbox5.wordWrap = true; // true: Send words that span the right edge to the next line. + _textbox5.text = "return:no, tab:no"; + _textbox5.parent = this; + } +} + +void main() +{ + Application.run(new MainForm()); +} diff --git a/source/dfl/application.d b/source/dfl/application.d index 4955c33..1937acf 100644 --- a/source/dfl/application.d +++ b/source/dfl/application.d @@ -2540,7 +2540,7 @@ extern(Windows) } -WNDPROC _superClass(HINSTANCE hinst, Dstring className, Dstring newClassName, out WNDCLASSA getInfo) // deprecated +deprecated WNDPROC _superClass(HINSTANCE hinst, Dstring className, Dstring newClassName, out WNDCLASSA getInfo) { WNDPROC wndProc; diff --git a/source/dfl/control.d b/source/dfl/control.d index 7088687..8b93feb 100644 --- a/source/dfl/control.d +++ b/source/dfl/control.d @@ -5,13 +5,16 @@ /// module dfl.control; -private import dfl.internal.dlib, dfl.internal.clib; - private import dfl.base, dfl.form, dfl.drawing; -private import dfl.internal.winapi, dfl.application, dfl.event, dfl.label; -private import dfl.internal.wincom, dfl.internal.utf, dfl.collections, dfl.internal.com; +private import dfl.application, dfl.event, dfl.label; +private import dfl.collections; private import core.memory; +private import dfl.internal.dlib, dfl.internal.clib; +private import dfl.internal.winapi, dfl.internal.wincom; +private import dfl.internal.utf; +private import dfl.internal.com; + version(NO_DRAG_DROP) version = DFL_NO_DRAG_DROP; @@ -178,9 +181,6 @@ enum ControlStyles: uint //DOUBLE_BUFFER = 0x10000, // TODO: implement. //OPTIMIZED_DOUBLE_BUFFER = 0x20000, // TODO: implement. USE_TEXT_FOR_ACCESSIBILITY = 0x40000, /// ditto - - WANT_TAB_KEY = 0x01000000, - WANT_ALL_KEYS = 0x02000000, } @@ -3704,11 +3704,92 @@ class Control: DObject, IWindow // docmain } - /// - // Return true if processed. + /// Process shortcut key (ctrl+A etc). + // Returns false when parent is none, otherwise call parent form's one. + protected bool processCmdKey(ref Message msg, Keys keyData) + { + if (parent) + return parent.processCmdKey(msg, keyData); + else + return false; + } + + /// Process dialog key (TAB, RETURN, ESC, UP, DOWN, LEFT, RIGHT and so on). + // Returns false when parent is none, otherwise call parent form's one. + protected bool processDialogKey(Keys keyData) + { + if (parent) + return parent.processDialogKey(keyData); + else + return false; + } + + /// Process mnemonic (access key) such as Alt+T. + // Returns false when parent is none, otherwise call parent form's one. + protected bool processDialogChar(char charCode) + { + if (parent) + return parent.processDialogChar(charCode); + else + return false; + } + + /// Pre-process keybord message + // This function called from Application.DflWndProc(). + // If return true, processed message, + // then wndProc() be not called after preProcessMessage(). + // If return false, wndProc() be called after preProcessMessage(). bool preProcessMessage(ref Message msg) { - return false; + bool result/+ = false+/; + + if (msg.msg == WM_KEYDOWN || msg.msg == WM_SYSKEYDOWN) + { + // TODO: Implement + // if (!getExtendedState(ExtendedStates.UiCues)) + // { + // processUICues(msg); + // } + + Keys keyData = cast(Keys)msg.wParam | modifierKeys; + + // processCmdKey returns true when keyData is a shortcut key (ctrl+C etc). + if (processCmdKey(msg, keyData)) + { + result = true; + } + // isInputKey returns true when keyData is a regular input key. + else if (isInputKey(keyData)) + { + // TODO: Implement + // SetExtendedState(ExtendedStates.InputKey, true); + result = false; // End preprocessing and call wndProc(). + } + else + { + // Process dialog keys such as TAB, RETURN, ESC, UP, DOWN, RIGHT, LEFT. + result = processDialogKey(keyData); + } + } + else if (msg.msg == WM_CHAR || msg.msg == WM_SYSCHAR) + { + if (msg.msg == WM_CHAR && isInputChar(cast(char)msg.wParam)) + { + // TODO: Implement + // setExtendedState(ExtendedStates.InputChar, true); + result = false; + } + else + { + result = processDialogChar(cast(char)msg.wParam); + } + } + else + { + result = false; + } + + return result; } @@ -3909,7 +3990,7 @@ class Control: DObject, IWindow // docmain } - package static void _dlgselnext(Form dlg, HWND hwcursel, bool forward, + package static bool _dlgselnext(Form dlg, HWND hwcursel, bool forward, bool tabStopOnly = true, bool selectableOnly = false, bool nested = true, bool wrap = true, HWND hwchildrenof = null) @@ -3960,6 +4041,7 @@ class Control: DObject, IWindow // docmain { //DefDlgProcA(dlg.handle, WM_NEXTDLGCTL, cast(WPARAM)hwfirst, MAKELPARAM(true, 0)); dlg._selectChild(hwfirst); + return true; } } } @@ -3990,22 +4072,26 @@ class Control: DObject, IWindow // docmain }, nested); // If it falls through without finding hwcursel, let it select the last one, even if not wrapping. if(HWND.init != hwprev) + { //DefDlgProcA(dlg.handle, WM_NEXTDLGCTL, cast(WPARAM)hwprev, MAKELPARAM(true, 0)); dlg._selectChild(hwprev); + return true; + } } + return false; } - package final void _selectNextControl(Form ctrltoplevel, + package final bool _selectNextControl(Form ctrltoplevel, Control ctrl, bool forward, bool tabStopOnly, bool nested, bool wrap) { if(!created) - return; + return false; assert(ctrltoplevel !is null); assert(ctrltoplevel.isHandleCreated); - _dlgselnext(ctrltoplevel, + return _dlgselnext(ctrltoplevel, (ctrl && ctrl.isHandleCreated) ? ctrl.handle : null, forward, tabStopOnly, !tabStopOnly, nested, wrap, this.handle); @@ -4019,14 +4105,16 @@ class Control: DObject, IWindow // docmain // Only considers child controls of this control. - final void selectNextControl(Control ctrl, bool forward, bool tabStopOnly, bool nested, bool wrap) + final bool selectNextControl(Control ctrl, bool forward, bool tabStopOnly, bool nested, bool wrap) { if(!created) - return; + return false; auto ctrltoplevel = findForm(); if(ctrltoplevel) return _selectNextControl(ctrltoplevel, ctrl, forward, tabStopOnly, nested, wrap); + + return false; } @@ -4594,7 +4682,26 @@ class Control: DObject, IWindow // docmain DWORD ldlgcode = 0; } - + + /// + protected bool processKeyPreview(ref Message m) + { + return parent && parent.processKeyPreview(m); + } + + + /// + // Returns true in order to break in wndProc(), because processed. + protected final bool processKeyMessage(ref Message m) + { + if ( parent && parent.processKeyPreview(m)) + { + return true; + } + return processKeyEventArgs(m); + } + + /// protected void wndProc(ref Message msg) { @@ -4845,7 +4952,7 @@ class Control: DObject, IWindow // docmain case WM_KEYUP: case WM_CHAR: case WM_SYSKEYDOWN: - case WM_SYSKEYUP: + case WM_SYSKEYUP: // TODO: Is it correct? case WM_SYSCHAR: //case WM_IMECHAR: /+ @@ -4858,7 +4965,14 @@ class Control: DObject, IWindow // docmain msg.result = 1; // The key was not processed. break; +/ - msg.result = !processKeyEventArgs(msg); + if (processKeyMessage(msg)) + { + //msg.result = 0; + return; + } + defWndProc(msg); + + //msg.result = !processKeyEventArgs(msg); return; case WM_MOUSEWHEEL: // Requires Windows 98 or NT4. @@ -5450,9 +5564,6 @@ class Control: DObject, IWindow // docmain defWndProc(msg); - if(ctrlStyle & ControlStyles.WANT_ALL_KEYS) - msg.result |= DLGC_WANTALLKEYS; - // Only want chars if ALT isn't down, because it would break mnemonics. if(!(GetKeyState(VK_MENU) & 0x8000)) msg.result |= DLGC_WANTCHARS; @@ -6802,7 +6913,6 @@ class Control: DObject, IWindow // docmain cbits &= ~CBits.IN_LAYOUT; } - /+ // Not sure what to do here. deprecated bool isInputChar(char charCode) @@ -6810,8 +6920,58 @@ class Control: DObject, IWindow // docmain return false; } +/ - - + protected bool isInputChar(char charCode) + { + int mask = 0; + if (charCode == Keys.TAB) + { + mask = (DLGC_WANTCHARS | DLGC_WANTALLKEYS | DLGC_WANTTAB); + } + else + { + mask = (DLGC_WANTCHARS | DLGC_WANTALLKEYS); + } + + return (SendMessage(hwnd, WM_GETDLGCODE, 0, 0) & mask) != 0; + } + + + /// isInputKey returns true when keyData is a regular input key. + // If keyData is input key, then window message is sended to wndProc() + // such as WM_KEYDOWN, WM_KEYUP, WM_CHAR and so on. + protected bool isInputKey(Keys keyData) + { + if ((keyData & Keys.ALT) == Keys.ALT) + { + return false; + } + + auto mask = DLGC_WANTALLKEYS; + switch (keyData & Keys.KEY_CODE) + { + case Keys.TAB: + mask = DLGC_WANTALLKEYS | DLGC_WANTTAB; + break; + case Keys.LEFT: + case Keys.RIGHT: + case Keys.UP: + case Keys.DOWN: + mask = DLGC_WANTALLKEYS | DLGC_WANTARROWS; + break; + default: + // Nothing + } + + // return isHandleCreated + // && (SendMessage(hwnd, WM_GETDLGCODE, 0, 0) & mask) != 0; + if (isHandleCreated) + { + LRESULT r = SendMessage(hwnd, WM_GETDLGCODE, 0, 0); + return (r & mask) != 0; + } + return false; + } + /// void setVisibleCore(bool byes) { @@ -6847,9 +7007,11 @@ class Control: DObject, IWindow // docmain package final bool _wantTabKey() { - if(ctrlStyle & ControlStyles.WANT_TAB_KEY) + // if(ctrlStyle & ControlStyles.WANT_TAB_KEY) + if((DLGC_WANTTAB & sendMessage(hwnd, WM_GETDLGCODE, 0, 0)) != 0) return true; - return false; + else + return false; } @@ -6857,12 +7019,22 @@ class Control: DObject, IWindow // docmain // Return true if processed. protected bool processKeyEventArgs(ref Message msg) { + // TODO: implement more (IME etc...) + switch(msg.msg) { + case WM_CHAR: + case WM_SYSCHAR: + { + scope KeyPressEventArgs kpea = new KeyPressEventArgs(cast(dchar)msg.wParam, modifierKeys); + onKeyPress(kpea); + return kpea.handled; + } + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { scope KeyEventArgs kea = new KeyEventArgs(cast(Keys)(msg.wParam | modifierKeys)); - ushort repeat = msg.lParam & 0xFFFF; // First 16 bits. for(; repeat; repeat--) { @@ -6870,35 +7042,21 @@ class Control: DObject, IWindow // docmain onKeyDown(kea); } - if(kea.handled) - return true; + return kea.handled; } - break; case WM_KEYUP: + case WM_SYSKEYUP: // TODO: Is it correct? { // Repeat count is always 1 for key up. scope KeyEventArgs kea = new KeyEventArgs(cast(Keys)(msg.wParam | modifierKeys)); onKeyUp(kea); - if(kea.handled) - return true; + return kea.handled; } - break; - - case WM_CHAR: - { - scope KeyPressEventArgs kpea = new KeyPressEventArgs(cast(dchar)msg.wParam, modifierKeys); - onKeyPress(kpea); - if(kpea.handled) - return true; - } - break; default: + assert(0); // Does not reached here, if WM is traped in Control.wndProc() correctly. } - - defWndProc(msg); - return !msg.result; } @@ -6907,25 +7065,7 @@ class Control: DObject, IWindow // docmain return processKeyEventArgs(msg); } - - /+ - bool processKeyPreview(ref Message m) - { - if(wparent) - return wparent.processKeyPreview(m); - return false; - } - - - protected bool processDialogChar(dchar charCode) - { - if(wparent) - return wparent.processDialogChar(charCode); - return false; - } - +/ - - + /// protected bool processMnemonic(dchar charCode) { @@ -8197,182 +8337,24 @@ class ContainerControl: ScrollableControl, IContainerControl // docmain } - protected: - /+ - override bool processDialogChar(char charCode) - { - // Not sure if this is correct. - return false; - } - +/ - - - /+ - deprecated protected override bool processMnemonic(dchar charCode) - { - return false; - } - - - bool processTabKey(bool forward) + protected bool processTabKey(bool forward) { if(isHandleCreated) { - //SendMessageA(hwnd, WM_NEXTDLGCTL, !forward, 0); - //return true; - select(true, forward); + return selectNextControl(activeControl, forward, tabStop, true, false); } return false; } - +/ } -import std.traits, std.typecons; private template hasLocalAliasing(T...) { + import std.traits, std.typecons; + static if( !T.length ) enum hasLocalAliasing = false; else enum hasLocalAliasing = std.traits.hasLocalAliasing!(T[0]) || dfl.control.hasLocalAliasing!(T[1 .. $]); } - -/// -shared class SharedControl -{ -private: - Control _ctrl; - - LPARAM makeParam(ARGS...)(void function(Control, ARGS) fn, Tuple!(ARGS)* args) - if (ARGS.length) - { - static assert((DflInvokeParam*).sizeof <= LPARAM.sizeof); - static struct InvokeParam - { - void function(Control, ARGS) fn; - ARGS args; - } - alias dfl.internal.clib.malloc malloc; - alias dfl.internal.clib.free free; - - auto param = cast(InvokeParam*)malloc(InvokeParam.sizeof); - param.fn = fn; - param.args = args.field; - - if (!param) - throw new OomException(); - - auto p = cast(DflInvokeParam*)malloc(DflInvokeParam.sizeof); - - if (!p) - throw new OomException(); - - - static void fnentry(Control c, size_t[] p) - { - auto param = cast(InvokeParam*)p[0]; - param.fn(c, param.args); - free(param); - } - - p.fp = &fnentry; - p.nparams = 1; - p.params[0] = cast(size_t)param; - - return cast(LPARAM)p; - } - - - LPARAM makeParamNoneArgs(void function(Control) fn) - { - static assert((DflInvokeParam*).sizeof <= LPARAM.sizeof); - alias dfl.internal.clib.malloc malloc; - alias dfl.internal.clib.free free; - - auto p = cast(DflInvokeParam*)malloc(DflInvokeParam.sizeof); - - if (!p) - throw new OomException(); - - static void fnentry(Control c, size_t[] p) - { - auto fn = cast(void function(Control))p[0]; - fn(c); - } - - p.fp = &fnentry; - p.nparams = 1; - p.params[0] = cast(size_t)fn; - - return cast(LPARAM)p; - } - - - -public: - /// - this(Control ctrl) - { - assert(ctrl); - _ctrl = cast(shared)ctrl; - } - - /// - void invoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) - if (ARGS.length && !hasLocalAliasing!(ARGS)) - { - auto ctrl = cast(Control)_ctrl; - auto hwnd = ctrl.handle; - - if(!hwnd) - Control.badInvokeHandle(); - - auto t = tuple(args); - auto p = makeParam(fn, &t); - SendMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); - } - - /// - void invoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) - if (!ARGS.length) - { - auto ctrl = cast(Control)_ctrl; - auto hwnd = ctrl.handle; - - if(!hwnd) - Control.badInvokeHandle(); - - auto p = makeParamNoneArgs(fn); - SendMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); - } - - /// - void delayInvoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) - if (ARGS.length && !hasLocalAliasing!(ARGS)) - { - auto ctrl = cast(Control)_ctrl; - auto hwnd = ctrl.handle; - - if(!hwnd) - Control.badInvokeHandle(); - - auto t = tuple(args); - auto p = makeParam(fn, &t); - PostMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); - } - - /// - void delayInvoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) - if (!ARGS.length) - { - auto ctrl = cast(Control)_ctrl; - auto hwnd = ctrl.handle; - - if(!hwnd) - Control.badInvokeHandle(); - - auto p = makeParamNoneArgs(fn); - PostMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); - } -} diff --git a/source/dfl/form.d b/source/dfl/form.d index ebacc7b..6f7435a 100644 --- a/source/dfl/form.d +++ b/source/dfl/form.d @@ -6,10 +6,13 @@ module dfl.form; private import dfl.internal.dlib; +private import dfl.internal.winapi; +private import dfl.internal.utf; -private import dfl.control, dfl.internal.winapi, dfl.event, dfl.drawing; -private import dfl.application, dfl.base, dfl.internal.utf; +private import dfl.control, dfl.event, dfl.drawing; +private import dfl.application, dfl.base; private import dfl.collections; + debug(APP_PRINT) { private import dfl.internal.clib; @@ -129,10 +132,10 @@ class Form: ContainerControl, IDialogResult // docmain /// final @property void acceptButton(IButtonControl btn) // setter { - if(acceptBtn) - acceptBtn.notifyDefault(false); + if(_acceptButton) + _acceptButton.notifyDefault(false); - acceptBtn = btn; + _acceptButton = btn; if(btn) btn.notifyDefault(true); @@ -141,14 +144,14 @@ class Form: ContainerControl, IDialogResult // docmain /// ditto final @property IButtonControl acceptButton() // getter { - return acceptBtn; + return _acceptButton; } /// final @property void cancelButton(IButtonControl btn) // setter { - cancelBtn = btn; + _cancelButton = btn; if(btn) { @@ -162,7 +165,7 @@ class Form: ContainerControl, IDialogResult // docmain /// ditto final @property IButtonControl cancelButton() // getter { - return cancelBtn; + return _cancelButton; } @@ -462,7 +465,8 @@ class Form: ContainerControl, IDialogResult // docmain debug { static import std.string; - er = std.string.format("CreateWindowEx failed {className=%s;exStyle=0x%X;style=0x%X;parent=0x%X;menu=0x%X;inst=0x%X;}", + er = std.string.format( + "CreateWindowEx failed {className=%s;exStyle=0x%X;style=0x%X;parent=0x%X;menu=0x%X;inst=0x%X;}", className, exStyle, style, cast(void*)parent, cast(void*)menu, cast(void*)inst); } goto create_err; @@ -918,10 +922,29 @@ class Form: ContainerControl, IDialogResult // docmain return wicon; } - - // TODO: implement. - // keyPreview - + /// keyPreview + final @property bool keyPreview() // getter + { + return _keyPreview; + } + + /// ditto + final @property void keyPreview(bool v) // setter + { + _keyPreview = v; + } + + /// + protected override bool processKeyPreview(ref Message m) + { + if(_keyPreview && processKeyEventArgs(m)) + { + // Returns true for call onKeyPress(), onKeyDown() or onKeyUp() on this form. + return true; + } + + return super.processKeyPreview(m); + } /// final @property bool isMdiChild() // getter @@ -2273,20 +2296,20 @@ class Form: ContainerControl, IDialogResult // docmain switch(LOWORD(msg.wParam)) { case IDOK: - if(acceptBtn) + if(_acceptButton) { if(HIWORD(msg.wParam) == BN_CLICKED) - acceptBtn.performClick(); + _acceptButton.performClick(); return; } break; //return; case IDCANCEL: - if(cancelBtn) + if(_cancelButton) { if(HIWORD(msg.wParam) == BN_CLICKED) - cancelBtn.performClick(); + _cancelButton.performClick(); return; } break; @@ -2742,8 +2765,139 @@ class Form: ContainerControl, IDialogResult // docmain } } + /// Process shortcut key (ctrl+A etc). + // Returns true when processed, false when not. + protected override bool processCmdKey(ref Message msg, Keys keyData) + { + if (super.processCmdKey(msg, keyData)) + { + return true; + } + + // Process MDI accelerator keys. + bool retValue = false; + MSG win32Message = msg._winMsg; + if ((_mdiClient !is null) && (_mdiClient.handle !is/+ != +/ null) && + TranslateMDISysAccel(_mdiClient.hwnd, &win32Message)) + { + retValue = true; + } + + msg.msg = win32Message.message; + msg.wParam = win32Message.wParam; + msg.lParam = win32Message.lParam; + msg.hWnd = win32Message.hwnd; + + return retValue; + } - package alias dfl.internal.utf.defDlgProc _defFormProc; + /// Process dialog key (TAB, RETURN, ESC, UP, DOWN, LEFT, RIGHT and so on). + // Returns true when processed, false when called super class. + protected override bool processDialogKey(Keys keyData) + { + if ((keyData & (Keys.ALT | Keys.CONTROL)) == Keys.NONE) + { + Keys keyCode = keyData & Keys.KEY_CODE; + IButtonControl button; + + switch (keyCode) + { + case Keys.RETURN: + // button = cast(IButtonControl)GetObject(propDefaultButton); + button = _acceptButton; + if (button) + { + //PerformClick now checks for validationcancelled... + //if (button is Control) + { + button.performClick(); + } + + return true; + } + + break; + case Keys.ESCAPE: + // button = cast(IButtonControl)GetObject(propCancelButton); + button = _cancelButton; + if (button) + { + // In order to keep the behavior in sync with native + // and MFC dialogs, we want to not give the cancel button + // the focus on Escape. If we do, we end up with giving it + // the focus when we reshow the dialog. + + //if (button is Control) { + // ((Control)button).Focus(); + //} + button.performClick(); + return true; + } + + break; + default: + // Fall through + } + } + + return super.processDialogKey(keyData); + } + + /// Process mnemonic (access key) such as Alt+T. + // Returns true when processed, otherwise call super class. + protected override bool processDialogChar(char charCode) + { + // If we're the top-level form or control, we need to do the mnemonic handling + // + if (isMdiChild && charCode != ' ') + { + if (processMnemonic(charCode)) + { + return true; + } + + // ContainerControl calls ProcessMnemonic starting from the active MdiChild form (this) + // so let's flag it as processed. + _mnemonicProcessed = true; + try + { + return super.processDialogChar(charCode); + } + finally + { + _mnemonicProcessed = false; + } + } + + // Non-MdiChild form, just pass the call to ContainerControl. + return super.processDialogChar(charCode); + } + + /// Changes forcus by TAB + // Returns true when changed forcus, otherwise returns false. + protected override bool processTabKey(bool forward) + { + if (selectNextControl(activeControl, forward, true, true, true)) + { + return true; + } + + // I've added a special case for UserControls because they shouldn't cycle back to the + // beginning if they don't have a parent form, such as when they're on an ActiveXBridge. + if (isMdiChild || !parentForm) + { + bool selected = selectNextControl(null, forward, true, true, false); + + if (selected) + { + return true; + } + } + + return false; + } + + package alias _defFormProc = dfl.internal.utf.defDlgProc; protected override void defWndProc(ref Message msg) { @@ -2990,7 +3144,7 @@ class Form: ContainerControl, IDialogResult // docmain private: - IButtonControl acceptBtn, cancelBtn; + IButtonControl _acceptButton, _cancelButton; bool autoscale = true; Size autoscaleBase; DialogResult fresult = DialogResult.NONE; @@ -3018,7 +3172,10 @@ class Form: ContainerControl, IDialogResult // docmain Form wowner = null, wmdiparent = null; //bool _closingvisible; bool nofilter = false; - + bool _keyPreview = false; // Gets or sets a value indicating whether the form will receive key events + // before the event is passed to the control that has focus. + bool _mnemonicProcessed = false; + version(NO_MDI) {} else { MdiClient _mdiClient = null; // null == not MDI container. @@ -3027,13 +3184,15 @@ class Form: ContainerControl, IDialogResult // docmain package static bool wantsAllKeys(HWND hwnd) { - return (SendMessageA(hwnd, WM_GETDLGCODE, 0, 0) & - DLGC_WANTALLKEYS) != 0; + return (SendMessageA(hwnd, WM_GETDLGCODE, 0, 0) & DLGC_WANTALLKEYS) != 0; } private static class FormMessageFilter: IMessageFilter { + /// Filters out a message before it is dispatched. + // Returns: true when filter the message and stop it from being dispatched; + // false when allow the message and continue to the next filter or control. protected bool preFilterMessage(ref Message m) { version(NO_MDI) @@ -3086,13 +3245,11 @@ class Form: ContainerControl, IDialogResult // docmain case WM_CHAR: switch(cast(Keys)m.wParam) { - case Keys.ENTER: - if(form.acceptButton) - { - dfl.internal.utf.isDialogMessage(form.handle, &m._winMsg); - return true; // Prevent. - } - return false; + // case Keys.ENTER: + // This case has been moved to wndProc(). + // If parent form does not have a default button, + // input RETURN to TextBox regardless of + // the value of TextBox.acceptsReturn. case Keys.ESCAPE: if(form.cancelButton) @@ -3145,22 +3302,28 @@ class Form: ContainerControl, IDialogResult // docmain case Keys.TAB: { - LRESULT dlgc; - Control cc; - dlgc = SendMessageA(m.hWnd, WM_GETDLGCODE, 0, 0); - cc = fromHandle(m.hWnd); + // When window message is WM_KEYDOWN, WM_KEYUP and WM_CHAR, + // wParam is not contain modifier keys such as CONTROL, SHIFT. + // So uses GetKeyState(). + + LRESULT dlgc = SendMessageA(m.hWnd, WM_GETDLGCODE, 0, 0); + Control cc = fromHandle(m.hWnd); + bool isPressingControl = (GetKeyState(VK_CONTROL) & 0x8000) != 0; if(cc) { - if(cc._wantTabKey()) + // When CONTROL is pressing, do not input TAB but change forcus. + if(cc._wantTabKey() && !isPressingControl) return false; // Continue. } else { - if(dlgc & DLGC_WANTALLKEYS) + // When CONTROL is pressing, do not input TAB but change forcus. + if((dlgc & DLGC_WANTALLKEYS) && !isPressingControl) return false; // Continue. } - //if(dlgc & (DLGC_WANTTAB | DLGC_WANTALLKEYS)) - if(dlgc & DLGC_WANTTAB) + + // When CONTROL is pressing, do not input TAB but change forcus. + if((dlgc & DLGC_WANTTAB) && !isPressingControl) return false; // Continue. if(WM_KEYDOWN == m.msg) { @@ -3302,9 +3465,9 @@ class Form: ContainerControl, IDialogResult // docmain /+ package final bool _dlgescape() { - if(cancelBtn) + if(_cancelButton) { - cancelBtn.performClick(); + _cancelButton.performClick(); return true; } return false; diff --git a/source/dfl/package.d b/source/dfl/package.d index d7d3ec6..3f46054 100644 --- a/source/dfl/package.d +++ b/source/dfl/package.d @@ -8,10 +8,10 @@ module dfl; public import dfl.base, dfl.menu, dfl.control, dfl.usercontrol, dfl.form, dfl.drawing, dfl.panel, dfl.event, dfl.application, dfl.button, dfl.socket, - dfl.timer, dfl.environment, dfl.label, dfl.textbox, + dfl.timer, dfl.environment, dfl.label, dfl.textboxbase, dfl.textbox, dfl.listbox, dfl.splitter, dfl.groupbox, dfl.messagebox, dfl.registry, dfl.notifyicon, dfl.collections, dfl.data, dfl.clipboard, dfl.commondialog, dfl.richtextbox, dfl.tooltip, dfl.combobox, dfl.treeview, dfl.picturebox, dfl.tabcontrol, dfl.listview, dfl.statusbar, dfl.progressbar, dfl.resources, - dfl.imagelist, dfl.toolbar, dfl.trackbar; + dfl.imagelist, dfl.toolbar, dfl.trackbar, dfl.sharedcontrol; diff --git a/source/dfl/richtextbox.d b/source/dfl/richtextbox.d index fb0d773..f3b1788 100644 --- a/source/dfl/richtextbox.d +++ b/source/dfl/richtextbox.d @@ -5,9 +5,14 @@ /// module dfl.richtextbox; -private import dfl.textbox, dfl.internal.winapi, dfl.event, dfl.application; -private import dfl.base, dfl.drawing, dfl.data; -private import dfl.control, dfl.internal.utf, dfl.internal.dlib; +private import dfl.textboxbase; +private import dfl.base, dfl.application; +private import dfl.event, dfl.drawing, dfl.data; +private import dfl.control; + +private import dfl.internal.winapi; +private import dfl.internal.utf; +private import dfl.internal.dlib; version(DFL_NO_MENUS) { @@ -118,7 +123,7 @@ class RichTextBox: TextBoxBase // docmain return wcurs; // Do return null and don't inherit. } - alias TextBoxBase.cursor cursor; // Overload. + alias cursor = TextBoxBase.cursor; // Overload. override @property Dstring selectedText() // getter @@ -138,7 +143,7 @@ class RichTextBox: TextBoxBase // docmain return null; } - alias TextBoxBase.selectedText selectedText; // Overload. + alias selectedText = TextBoxBase.selectedText; // Overload. override @property void selectionLength(uint len) // setter @@ -200,13 +205,13 @@ class RichTextBox: TextBoxBase // docmain override @property void maxLength(uint len) // setter { - lim = len; + _lim = len; if(created) SendMessageA(handle, EM_EXLIMITTEXT, 0, cast(LPARAM)len); } - alias TextBoxBase.maxLength maxLength; // Overload. + alias maxLength = TextBoxBase.maxLength; // Overload. override @property Size defaultSize() // getter @@ -233,7 +238,7 @@ class RichTextBox: TextBoxBase // docmain super.backColor(c); } - alias TextBoxBase.backColor backColor; // Overload. + alias backColor = TextBoxBase.backColor; // Overload. private void _setfc(Color c) @@ -260,7 +265,7 @@ class RichTextBox: TextBoxBase // docmain super.foreColor(c); } - alias TextBoxBase.foreColor foreColor; // Overload. + alias foreColor = TextBoxBase.foreColor; // Overload. /// @@ -303,7 +308,7 @@ class RichTextBox: TextBoxBase // docmain } } - alias TextBoxBase.paste paste; // Overload. + alias paste = TextBoxBase.paste; // Overload. /// diff --git a/source/dfl/sharedcontrol.d b/source/dfl/sharedcontrol.d new file mode 100644 index 0000000..d5a5819 --- /dev/null +++ b/source/dfl/sharedcontrol.d @@ -0,0 +1,152 @@ +// Written by Christopher E. Miller +// See the included license.txt for copyright and license details. + + +/// +module dfl.sharedcontrol; + +private import dfl.control; +private import dfl.application; + +private import dfl.internal.winapi; +private import dfl.internal.dlib; +private import dfl.internal.clib; + +/// +shared class SharedControl +{ +private: + Control _ctrl; + + LPARAM makeParam(ARGS...)(void function(Control, ARGS) fn, Tuple!(ARGS)* args) + if (ARGS.length) + { + static assert((DflInvokeParam*).sizeof <= LPARAM.sizeof); + static struct InvokeParam + { + void function(Control, ARGS) fn; + ARGS args; + } + alias dfl.internal.clib.malloc malloc; + alias dfl.internal.clib.free free; + + auto param = cast(InvokeParam*)malloc(InvokeParam.sizeof); + param.fn = fn; + param.args = args.field; + + if (!param) + throw new OomException(); + + auto p = cast(DflInvokeParam*)malloc(DflInvokeParam.sizeof); + + if (!p) + throw new OomException(); + + + static void fnentry(Control c, size_t[] p) + { + auto param = cast(InvokeParam*)p[0]; + param.fn(c, param.args); + free(param); + } + + p.fp = &fnentry; + p.nparams = 1; + p.params[0] = cast(size_t)param; + + return cast(LPARAM)p; + } + + + LPARAM makeParamNoneArgs(void function(Control) fn) + { + static assert((DflInvokeParam*).sizeof <= LPARAM.sizeof); + alias dfl.internal.clib.malloc malloc; + alias dfl.internal.clib.free free; + + auto p = cast(DflInvokeParam*)malloc(DflInvokeParam.sizeof); + + if (!p) + throw new OomException(); + + static void fnentry(Control c, size_t[] p) + { + auto fn = cast(void function(Control))p[0]; + fn(c); + } + + p.fp = &fnentry; + p.nparams = 1; + p.params[0] = cast(size_t)fn; + + return cast(LPARAM)p; + } + + + +public: + /// + this(Control ctrl) + { + assert(ctrl); + _ctrl = cast(shared)ctrl; + } + + /// + void invoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) + if (ARGS.length && !hasLocalAliasing!(ARGS)) + { + auto ctrl = cast(Control)_ctrl; + auto hwnd = ctrl.handle; + + if(!hwnd) + Control.badInvokeHandle(); + + auto t = tuple(args); + auto p = makeParam(fn, &t); + SendMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); + } + + /// + void invoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) + if (!ARGS.length) + { + auto ctrl = cast(Control)_ctrl; + auto hwnd = ctrl.handle; + + if(!hwnd) + Control.badInvokeHandle(); + + auto p = makeParamNoneArgs(fn); + SendMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); + } + + /// + void delayInvoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) + if (ARGS.length && !hasLocalAliasing!(ARGS)) + { + auto ctrl = cast(Control)_ctrl; + auto hwnd = ctrl.handle; + + if(!hwnd) + Control.badInvokeHandle(); + + auto t = tuple(args); + auto p = makeParam(fn, &t); + PostMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); + } + + /// + void delayInvoke(ARGS...)(void function(Control, ARGS) fn, ARGS args) + if (!ARGS.length) + { + auto ctrl = cast(Control)_ctrl; + auto hwnd = ctrl.handle; + + if(!hwnd) + Control.badInvokeHandle(); + + auto p = makeParamNoneArgs(fn); + PostMessageA(hwnd, wmDfl, WPARAM_DFL_DELAY_INVOKE_PARAMS, p); + } +} diff --git a/source/dfl/textbox.d b/source/dfl/textbox.d index ee04f74..a3efab6 100644 --- a/source/dfl/textbox.d +++ b/source/dfl/textbox.d @@ -5,10 +5,14 @@ /// module dfl.textbox; -private import dfl.internal.dlib; +private import dfl.control, dfl.base, dfl.application; +private import dfl.drawing, dfl.event; +private import dfl.textboxbase; + +private import dfl.internal.dlib; +private import dfl.internal.winapi; +private import dfl.internal.utf; -private import dfl.control, dfl.base, dfl.internal.winapi, dfl.application; -private import dfl.drawing, dfl.event, dfl.internal.utf; debug(APP_PRINT) { private import dfl.internal.clib; @@ -29,1041 +33,40 @@ private extern(Windows) void _initTextBox(); // Note: ControlStyles.CACHE_TEXT might not work correctly with a text box. // It's not actually a bug, but a limitation of this control. -/// -abstract class TextBoxBase: ControlSuperClass // docmain -{ - /// - final @property void acceptsTab(bool byes) // setter - { - atab = byes; - setStyle(ControlStyles.WANT_TAB_KEY, atab); - } - - /// ditto - final @property bool acceptsTab() // getter - { - return atab; - } - - - /// - @property void borderStyle(BorderStyle bs) // setter - { - final switch(bs) - { - case BorderStyle.FIXED_3D: - _style(_style() & ~WS_BORDER); - _exStyle(_exStyle() | WS_EX_CLIENTEDGE); - break; - - case BorderStyle.FIXED_SINGLE: - _exStyle(_exStyle() & ~WS_EX_CLIENTEDGE); - _style(_style() | WS_BORDER); - break; - - case BorderStyle.NONE: - _style(_style() & ~WS_BORDER); - _exStyle(_exStyle() & ~WS_EX_CLIENTEDGE); - break; - } - - if(created) - { - redrawEntire(); - } - } - - /// ditto - @property BorderStyle borderStyle() // getter - { - if(_exStyle() & WS_EX_CLIENTEDGE) - return BorderStyle.FIXED_3D; - else if(_style() & WS_BORDER) - return BorderStyle.FIXED_SINGLE; - return BorderStyle.NONE; - } - - - /// - final @property bool canUndo() // getter - { - if(!created) - return false; - return SendMessageA(handle, EM_CANUNDO, 0, 0) != 0; - } - - - /// - final @property void hideSelection(bool byes) // setter - { - if(byes) - _style(_style() & ~ES_NOHIDESEL); - else - _style(_style() | ES_NOHIDESEL); - } - - /// ditto - final @property bool hideSelection() // getter - { - return (_style() & ES_NOHIDESEL) == 0; - } - - - /// - final @property void lines(Dstring[] lns) // setter - { - Dstring result; - foreach(Dstring s; lns) - { - result ~= s ~ "\r\n"; - } - if(result.length) // Remove last \r\n. - result = result[0 .. result.length - 2]; - text = result; - } - - /// ditto - final @property Dstring[] lines() // getter - { - return stringSplitLines(text); - } - - - /// - @property void maxLength(uint len) // setter - { - if(!len) - { - if(multiline) - lim = 0xFFFFFFFF; - else - lim = 0x7FFFFFFE; - } - else - { - lim = len; - } - - if(created) - { - Message m; - m = Message(handle, EM_SETLIMITTEXT, cast(WPARAM)lim, 0); - prevWndProc(m); - } - } - - /// ditto - @property uint maxLength() // getter - { - if(created) - lim = cast(uint)SendMessageA(handle, EM_GETLIMITTEXT, 0, 0); - return lim; - } - - - /// - final uint getLineCount() - { - if(!multiline) - return 1; - - if(created) - { - return cast(uint)SendMessageA(handle, EM_GETLINECOUNT, 0, 0); - } - - Dstring s; - size_t iw = 0; - uint count = 1; - s = text; - for(; iw != s.length; iw++) - { - if('\r' == s[iw]) - { - if(iw + 1 == s.length) - break; - if('\n' == s[iw + 1]) - { - iw++; - count++; - } - } - } - return count; - } - - - /// - final @property void modified(bool byes) // setter - { - if(created) - SendMessageA(handle, EM_SETMODIFY, byes, 0); - } - - /// ditto - final @property bool modified() // getter - { - if(!created) - return false; - return SendMessageA(handle, EM_GETMODIFY, 0, 0) != 0; - } - - - /// - @property void multiline(bool byes) // setter - { - /+ - if(byes) - _style(_style() & ~ES_AUTOHSCROLL | ES_MULTILINE); - else - _style(_style() & ~ES_MULTILINE | ES_AUTOHSCROLL); - +/ - - // TODO: check if correct implementation. - - LONG st; - - if(byes) - { - st = _style() | ES_MULTILINE | ES_AUTOVSCROLL; - - if(_wrap) - st &= ~ES_AUTOHSCROLL; - else - st |= ES_AUTOHSCROLL; - } - else - { - st = _style() & ~(ES_MULTILINE | ES_AUTOVSCROLL); - - // Always H-scroll when single line. - st |= ES_AUTOHSCROLL; - } - - _style(st); - - _crecreate(); - } - - /// ditto - @property bool multiline() // getter - { - return (_style() & ES_MULTILINE) != 0; - } - - - /// - final @property void readOnly(bool byes) // setter - { - if(created) - { - SendMessageA(handle, EM_SETREADONLY, byes, 0); // Should trigger WM_STYLECHANGED. - invalidate(); // ? - } - else - { - if(byes) - _style(_style() | ES_READONLY); - else - _style(_style() & ~ES_READONLY); - } - } - - /// ditto - final @property bool readOnly() // getter - { - return (_style() & ES_READONLY) != 0; - } - - - /// - @property void selectedText(Dstring sel) // setter - { - /+ - if(created) - SendMessageA(handle, EM_REPLACESEL, FALSE, cast(LPARAM)unsafeStringz(sel)); - +/ - - if(created) - { - //dfl.internal.utf.sendMessage(handle, EM_REPLACESEL, FALSE, sel); - dfl.internal.utf.sendMessageUnsafe(handle, EM_REPLACESEL, FALSE, sel); - } - } - - /// ditto - @property Dstring selectedText() // getter - { - /+ - if(created) - { - uint v1, v2; - SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); - if(v1 == v2) - return null; - assert(v2 > v1); - Dstring result = new char[v2 - v1 + 1]; - result[result.length - 1] = 0; - result = result[0 .. result.length - 1]; - result[] = text[v1 .. v2]; - return result; - } - return null; - +/ - - if(created) - return dfl.internal.utf.getSelectedText(handle); - return null; - } - - - /// - @property void selectionLength(uint len) // setter - { - if(created) - { - uint v1, v2; - SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); - v2 = v1 + len; - SendMessageA(handle, EM_SETSEL, v1, v2); - } - } - - /// ditto - // Current selection length, in characters. - // This does not necessarily correspond to the length of chars; some characters use multiple chars. - // An end of line (\r\n) takes up 2 characters. - @property uint selectionLength() // getter - { - if(created) - { - uint v1, v2; - SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); - assert(v2 >= v1); - return v2 - v1; - } - return 0; - } - - - /// - @property void selectionStart(uint pos) // setter - { - if(created) - { - uint v1, v2; - SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); - assert(v2 >= v1); - v2 = pos + (v2 - v1); - SendMessageA(handle, EM_SETSEL, pos, v2); - } - } - - /// ditto - // Current selection starting index, in characters. - // This does not necessarily correspond to the index of chars; some characters use multiple chars. - // An end of line (\r\n) takes up 2 characters. - @property uint selectionStart() // getter - { - if(created) - { - uint v1, v2; - SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); - return v1; - } - return 0; - } - - - /// - // Number of characters in the textbox. - // This does not necessarily correspond to the number of chars; some characters use multiple chars. - // An end of line (\r\n) takes up 2 characters. - // Return may be larger than the amount of characters. - // This is a lot faster than retrieving the text, but retrieving the text is completely accurate. - @property uint textLength() // getter - { - if(!(ctrlStyle & ControlStyles.CACHE_TEXT) && created()) - //return cast(uint)SendMessageA(handle, WM_GETTEXTLENGTH, 0, 0); - return cast(uint)dfl.internal.utf.sendMessage(handle, WM_GETTEXTLENGTH, 0, 0); - return wtext.length.toI32; - } - - - /// - @property final void wordWrap(bool byes) // setter - { - /+ - if(byes) - _style(_style() | ES_AUTOVSCROLL); - else - _style(_style() & ~ES_AUTOVSCROLL); - +/ - - // TODO: check if correct implementation. - - if(_wrap == byes) - return; - - _wrap = byes; - - // Always H-scroll when single line. - if(multiline) - { - if(byes) - { - _style(_style() & ~(ES_AUTOHSCROLL | WS_HSCROLL)); - } - else - { - LONG st; - st = _style(); - - st |= ES_AUTOHSCROLL; - - if(_hscroll) - st |= WS_HSCROLL; - - _style(st); - } - } - - _crecreate(); - } - - /// ditto - final @property bool wordWrap() // getter - { - //return (_style() & ES_AUTOVSCROLL) != 0; - - return _wrap; - } - - - /// - final void appendText(Dstring txt) - { - if(created) - { - selectionStart = textLength; - selectedText = txt; - } - else - { - text = text ~ txt; - } - } - - - /// - final void clear() - { - /+ - // WM_CLEAR only clears the selection ? - if(created) - SendMessageA(handle, WM_CLEAR, 0, 0); - else - wtext = null; - +/ - - text = null; - } - - - /// - final void clearUndo() - { - if(created) - SendMessageA(handle, EM_EMPTYUNDOBUFFER, 0, 0); - } - - - /// - final void copy() - { - if(created) - { - SendMessageA(handle, WM_COPY, 0, 0); - } - else - { - // There's never a selection if the window isn't created; so just empty the clipboard. - - if(!OpenClipboard(null)) - { - debug(APP_PRINT) - cprintf("Unable to OpenClipboard().\n"); - //throw new DflException("Unable to set clipboard data."); - return; - } - EmptyClipboard(); - CloseClipboard(); - } - } - - - /// - final void cut() - { - if(created) - { - SendMessageA(handle, WM_CUT, 0, 0); - } - else - { - // There's never a selection if the window isn't created; so just empty the clipboard. - - if(!OpenClipboard(null)) - { - debug(APP_PRINT) - cprintf("Unable to OpenClipboard().\n"); - //throw new DflException("Unable to set clipboard data."); - return; - } - EmptyClipboard(); - CloseClipboard(); - } - } - - - /// - final void paste() - { - if(created) - { - SendMessageA(handle, WM_PASTE, 0, 0); - } - else - { - // Can't do anything because there's no selection ? - } - } - - - /// - final void scrollToCaret() - { - if(created) - SendMessageA(handle, EM_SCROLLCARET, 0, 0); - } - - - /// - final void select(uint start, uint length) - { - if(created) - SendMessageA(handle, EM_SETSEL, start, start + length); - } - - alias Control.select select; // Overload. - - - /// - final void selectAll() - { - if(created) - SendMessageA(handle, EM_SETSEL, 0, -1); - } - - - override Dstring toString() - { - return text; // ? - } - - - /// - final void undo() - { - if(created) - SendMessageA(handle, EM_UNDO, 0, 0); - } - - - /+ - override void createHandle() - { - if(isHandleCreated) - return; - - createClassHandle(TEXTBOX_CLASSNAME); - - onHandleCreated(EventArgs.empty); - } - +/ - - - override void createHandle() - { - if(!isHandleCreated) - { - Dstring txt; - txt = wtext; - - super.createHandle(); - - //dfl.internal.utf.setWindowText(hwnd, txt); - text = txt; // So that it can be overridden. - } - } - - - protected override void createParams(ref CreateParams cp) - { - super.createParams(cp); - - cp.className = TEXTBOX_CLASSNAME; - cp.caption = null; // Set in createHandle() to allow larger buffers. - } - - - protected override void onHandleCreated(EventArgs ea) - { - super.onHandleCreated(ea); - - //SendMessageA(hwnd, EM_SETLIMITTEXT, cast(WPARAM)lim, 0); - maxLength = lim; // Call virtual function. - } - - - private - { - version(DFL_NO_MENUS) - { - } - else - { - void menuUndo(Object sender, EventArgs ea) - { - undo(); - } - - - void menuCut(Object sender, EventArgs ea) - { - cut(); - } - - - void menuCopy(Object sender, EventArgs ea) - { - copy(); - } - - - void menuPaste(Object sender, EventArgs ea) - { - paste(); - } - - - void menuDelete(Object sender, EventArgs ea) - { - // Only clear selection. - SendMessageA(handle, WM_CLEAR, 0, 0); - } - - - void menuSelectAll(Object sender, EventArgs ea) - { - selectAll(); - } - - - bool isClipboardText() - { - if(!OpenClipboard(handle)) - return false; - - bool result; - result = GetClipboardData(CF_TEXT) != null; - - CloseClipboard(); - - return result; - } - - - void menuPopup(Object sender, EventArgs ea) - { - int slen, tlen; - bool issel; - - slen = selectionLength; - tlen = textLength; - issel = slen != 0; - - miundo.enabled = canUndo; - micut.enabled = !readOnly() && issel; - micopy.enabled = issel; - mipaste.enabled = !readOnly() && isClipboardText(); - midel.enabled = !readOnly() && issel; - misel.enabled = tlen != 0 && tlen != slen; - } - - - MenuItem miundo, micut, micopy, mipaste, midel, misel; - } - } - - - this() - { - _initTextBox(); - - wstyle |= WS_TABSTOP | ES_AUTOHSCROLL; - wexstyle |= WS_EX_CLIENTEDGE; - ctrlStyle |= ControlStyles.SELECTABLE; - wclassStyle = textBoxClassStyle; - - version(DFL_NO_MENUS) - { - } - else - { - MenuItem mi; - - cmenu = new ContextMenu; - cmenu.popup ~= &menuPopup; - - miundo = new MenuItem; - miundo.text = "&Undo"; - miundo.click ~= &menuUndo; - miundo.index = 0; - cmenu.menuItems.add(miundo); - - mi = new MenuItem; - mi.text = "-"; - mi.index = 1; - cmenu.menuItems.add(mi); - - micut = new MenuItem; - micut.text = "Cu&t"; - micut.click ~= &menuCut; - micut.index = 2; - cmenu.menuItems.add(micut); - - micopy = new MenuItem; - micopy.text = "&Copy"; - micopy.click ~= &menuCopy; - micopy.index = 3; - cmenu.menuItems.add(micopy); - - mipaste = new MenuItem; - mipaste.text = "&Paste"; - mipaste.click ~= &menuPaste; - mipaste.index = 4; - cmenu.menuItems.add(mipaste); - - midel = new MenuItem; - midel.text = "&Delete"; - midel.click ~= &menuDelete; - midel.index = 5; - cmenu.menuItems.add(midel); - - mi = new MenuItem; - mi.text = "-"; - mi.index = 6; - cmenu.menuItems.add(mi); - - misel = new MenuItem; - misel.text = "Select &All"; - misel.click ~= &menuSelectAll; - misel.index = 7; - cmenu.menuItems.add(misel); - } - } - - - override @property Color backColor() // getter - { - if(Color.empty == backc) - return defaultBackColor; - return backc; - } - - alias Control.backColor backColor; // Overload. - - - static @property Color defaultBackColor() // getter - { - return Color.systemColor(COLOR_WINDOW); - } - - - override @property Color foreColor() // getter - { - if(Color.empty == forec) - return defaultForeColor; - return forec; - } - - alias Control.foreColor foreColor; // Overload. - - - static @property Color defaultForeColor() //getter - { - return Color.systemColor(COLOR_WINDOWTEXT); - } - - - override @property Cursor cursor() // getter - { - if(!wcurs) - return _defaultCursor; - return wcurs; - } - - alias Control.cursor cursor; // Overload. - - - /// - int getFirstCharIndexFromLine(int line) - { - if(!isHandleCreated) - return -1; // ... - if(line < 0) - return -1; - return SendMessageA(hwnd, EM_LINEINDEX, line, 0).toI32; - } - - /// ditto - int getFirstCharIndexOfCurrentLine() - { - if(!isHandleCreated) - return -1; // ... - return SendMessageA(hwnd, EM_LINEINDEX, -1, 0).toI32; - } - - - /// - int getLineFromCharIndex(int charIndex) - { - if(!isHandleCreated) - return -1; // ... - if(charIndex < 0) - return -1; - return SendMessageA(hwnd, EM_LINEFROMCHAR, charIndex, 0).toI32; - } - - - /// - Point getPositionFromCharIndex(int charIndex) - { - if(!isHandleCreated) - return Point(0, 0); // ... - if(charIndex < 0) - return Point(0, 0); - POINT point; - SendMessageA(hwnd, EM_POSFROMCHAR, cast(WPARAM)&point, charIndex); - return Point(point.x, point.y); - } - - /// ditto - int getCharIndexFromPosition(Point pt) - { - if(!isHandleCreated) - return -1; // ... - if(!multiline) - return 0; - auto lresult = SendMessageA(hwnd, EM_CHARFROMPOS, 0, MAKELPARAM(pt.x, pt.y)); - if(-1 == lresult) - return -1; - return cast(int)cast(short)(lresult & 0xFFFF); - } - - - package static @property Cursor _defaultCursor() // getter - { - static Cursor def = null; - - if(!def) - { - synchronized - { - if(!def) - def = new SafeCursor(LoadCursorA(null, IDC_IBEAM)); - } - } - - return def; - } - - - protected: - protected override void onReflectedMessage(ref Message m) - { - super.onReflectedMessage(m); - - switch(m.msg) - { - case WM_COMMAND: - switch(HIWORD(m.wParam)) - { - case EN_CHANGE: - onTextChanged(EventArgs.empty); - break; - - default: - } - break; - - /+ - case WM_CTLCOLORSTATIC: - case WM_CTLCOLOREDIT: - /+ - //SetBkColor(cast(HDC)m.wParam, backColor.toRgb()); // ? - SetBkMode(cast(HDC)m.wParam, OPAQUE); // ? - +/ - break; - +/ - - default: - } - } - - - override void prevWndProc(ref Message msg) - { - version(DFL_NO_MENUS) - { - // Don't prevent WM_CONTEXTMENU so at least it'll have a default menu. - } - else - { - if(msg.msg == WM_CONTEXTMENU) // Ignore the default context menu. - return; - } - - //msg.result = CallWindowProcA(textBoxPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); - msg.result = dfl.internal.utf.callWindowProc(textBoxPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); - } - - - protected override bool processKeyEventArgs(ref Message msg) // package - { - switch(msg.msg) - { - case WM_KEYDOWN: - case WM_KEYUP: - case WM_CHAR: - if('\t' == msg.wParam) - { - // TODO: fix this. This case shouldn't be needed. - if(atab) - { - if(super.processKeyEventArgs(msg)) - return true; // Handled. - if(WM_KEYDOWN == msg.msg) - { - if(multiline) // Only multiline textboxes can have real tabs.. - { - //selectedText = "\t"; - //SendMessageA(handle, EM_REPLACESEL, TRUE, cast(LPARAM)"\t".ptr); // Allow undo. // Crashes DMD 0.161. - auto str = "\t".ptr; - SendMessageA(handle, EM_REPLACESEL, TRUE, cast(LPARAM)str); // Allow undo. - } - } - return true; // Handled. - } - } - break; - - default: - } - return super.processKeyEventArgs(msg); - } - - - override void wndProc(ref Message msg) - { - switch(msg.msg) - { - case WM_GETDLGCODE: - super.wndProc(msg); - if(atab) - { - //if(GetKeyState(Keys.TAB) & 0x8000) - { - //msg.result |= DLGC_WANTALLKEYS; - msg.result |= DLGC_WANTTAB; - } - } - else - { - msg.result &= ~DLGC_WANTTAB; - } - return; - - default: - super.wndProc(msg); - } - } - - - override @property Size defaultSize() // getter - { - return Size(120, 23); // ? - } - - - private: - package uint lim = 30_000; // Documented as default. - bool _wrap = true; - bool _hscroll; - - bool atab = false; - - /+ - @property bool atab() // getter - { - if(_style() & X) - return true; - return false; - } - - @property void atab(bool byes) // setter - { - if(byes) - _style(_style() | X); - else - _style(_style() & ~X); - } - +/ - - - @property void hscroll(bool byes) // setter - { - _hscroll = byes; - - if(byes && (!_wrap || !multiline)) - _style(_style() | WS_HSCROLL | ES_AUTOHSCROLL); - } - - - @property bool hscroll() // getter - { - return _hscroll; - } -} - - /// class TextBox: TextBoxBase // docmain { /// final @property void acceptsReturn(bool byes) // setter { - if(byes) - _style(_style() | ES_WANTRETURN); - else - _style(_style() & ~ES_WANTRETURN); + // if(byes) + // { + // _style(_style() | ES_WANTRETURN); + // } + // else + // { + // _style(_style() & ~ES_WANTRETURN); + // } + if (_acceptsReturn == byes) return; + _acceptsReturn = byes; + if (multiline) + { + if (byes) + { + _style = _style | ES_WANTRETURN; + } + else + { + _style = _style & ~ES_WANTRETURN; + } + } } /// ditto final @property bool acceptsReturn() // getter { - return (_style() & ES_WANTRETURN) != 0; + // return (_style() & ES_WANTRETURN) != 0; + return _acceptsReturn; } @@ -1129,16 +132,16 @@ class TextBox: TextBoxBase // docmain _style(_style() & ~ES_PASSWORD); } - passchar = pwc; + _passchar = pwc; } /// ditto final @property dchar passwordChar() // getter { if(created) - //passchar = cast(dchar)SendMessageA(handle, EM_GETPASSWORDCHAR, 0, 0); - passchar = dfl.internal.utf.emGetPasswordChar(handle); - return passchar; + //_passchar = cast(dchar)SendMessageA(handle, EM_GETPASSWORDCHAR, 0, 0); + _passchar = dfl.internal.utf.emGetPasswordChar(handle); + return _passchar; } @@ -1253,18 +256,89 @@ class TextBox: TextBoxBase // docmain wstyle |= ES_LEFT; } - + /// protected override @property void onHandleCreated(EventArgs ea) { super.onHandleCreated(ea); - if(passchar) + if(_passchar) { - SendMessageA(hwnd, EM_SETPASSWORDCHAR, passchar, 0); + SendMessageA(hwnd, EM_SETPASSWORDCHAR, _passchar, 0); } } - + /// isInputKey returns true when keyData is a regular input key. + // If keyData is input key, then window message is sended to wndProc() + // such as WM_KEYDOWN, WM_KEYUP, WM_CHAR and so on. + protected override bool isInputKey(Keys keyData) + { + if (multiline && (keyData & Keys.ALT) == 0) + { + switch (keyData & Keys.KEY_CODE) + { + case Keys.RETURN: + { + IButtonControl b = findForm.acceptButton; + if(b is null) + return true; // When Form's default button is none, RETURN is always input key. + else if(b !is null && _acceptsReturn == false) + return super.isInputKey(keyData); + else if(b !is null && _acceptsReturn == true) + return true; + else + assert(0); + } + default: + // Fall through + } + } + + return super.isInputKey(keyData); + } + + // Process dialog key (TAB, RETURN, ESC, UP, DOWN, LEFT, RIGHT and so on). + // Returns true when processed, otherwise call super class. + protected override bool processDialogKey(Keys keyData) + { + Keys keyCode = cast(Keys)keyData & Keys.KEY_CODE; + + if (keyCode == Keys.RETURN) + { + if (_acceptsReturn && (keyData & Keys.CONTROL) != 0) + { + // When this control accepts Returns, Ctrl-Return is treated exactly like Return. + keyData &= ~Keys.CONTROL; + } + + IButtonControl b = findForm.acceptButton; + if(b is null) + return true; // When Form's default button is none, RETURN is always input key. + else if(b !is null && _acceptsReturn == false) + return super.processDialogKey(keyData); + else if(b !is null && _acceptsReturn == true) + return true; + else + assert(0); + } + return super.processDialogKey(keyData); + } + + /// Process shortcut key (ctrl+A etc). + // Returns true when processed, false when not. + protected override bool processCmdKey(ref Message m, Keys keyData) + { + bool isProcessed = super.processCmdKey(m, keyData); + + // TODO: Implement ShortcutsEnabled + if (!isProcessed && multiline /+&& ShortcutsEnabled+/ && (keyData == (Keys.CONTROL | Keys.A))) + { + selectAll(); + return true; + } + + return isProcessed; + } + /+ override @property void wndProc(ref Message msg) { @@ -1287,9 +361,73 @@ class TextBox: TextBoxBase // docmain super.wndProc(msg); } +/ + protected override void wndProc(ref Message msg) + { + switch(msg.msg) + { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_CHAR: + case WM_SYSCHAR: + case WM_KEYUP: + case WM_SYSKEYUP: + Keys keyCode = cast(Keys)msg.wParam & Keys.KEY_CODE; + + if (keyCode == Keys.RETURN) + { + IButtonControl b = findForm.acceptButton; + if(b is null) + super.wndProc(msg); + else if(b !is null && _acceptsReturn == false) + return; + else if(b !is null && _acceptsReturn == true) + super.wndProc(msg); + else + assert(0); + } + else + { + super.wndProc(msg); + } + return; + + case WM_GETDLGCODE: + Keys keyCode = cast(Keys)msg.wParam & Keys.KEY_CODE; + if(keyCode == Keys.RETURN) + { + // DLGC_WANTARROWS = 1 + // Want VK_LEFT, VK_RIGHT, VK_UP and VK_DOWN in WM_KEYDOWN. + // DLGC_WANTTAB = 2 + // Want VK_TAB in WM_KEYDOWN. + // DLGC_WANTALLKEYS = 4 + // Want VK_RETURN, VK_EXECUTE, VK_ESCAPE and VK_CANCEL in WM_KEYDOWN. + // DLGC_WANTCHARS = 0x80 + // Want WM_CHAR. + if(_acceptsReturn) + { + msg.result |= DLGC_WANTCHARS | DLGC_WANTALLKEYS | DLGC_WANTARROWS; + } + else + { + msg.result &= ~(DLGC_WANTCHARS | DLGC_WANTALLKEYS | DLGC_WANTARROWS); + } + return; // Do not call super.wndProc() because processing RETURN was done. + } + else + { + msg.result |= DLGC_WANTCHARS | DLGC_WANTARROWS; + super.wndProc(msg); + return; + } + + default: + super.wndProc(msg); + } + } private: - dchar passchar = 0; + dchar _passchar = 0; + bool _acceptsReturn = false; } diff --git a/source/dfl/textboxbase.d b/source/dfl/textboxbase.d new file mode 100644 index 0000000..412b592 --- /dev/null +++ b/source/dfl/textboxbase.d @@ -0,0 +1,1214 @@ +// Written by Christopher E. Miller +// See the included license.txt for copyright and license details. + + +/// +module dfl.textboxbase; + +private import dfl.control, dfl.base, dfl.application; +private import dfl.drawing, dfl.event; + +private import dfl.internal.dlib; +private import dfl.internal.winapi; +private import dfl.internal.utf; + +debug(APP_PRINT) +{ + private import dfl.internal.clib; +} + +version(DFL_NO_MENUS) +{ +} +else +{ + private import dfl.menu; +} + + +private extern(Windows) void _initTextBox(); + + +// Note: ControlStyles.CACHE_TEXT might not work correctly with a text box. +// It's not actually a bug, but a limitation of this control. + +/// +abstract class TextBoxBase: ControlSuperClass // docmain +{ + /// + final @property void acceptsTab(bool byes) // setter + { + if (_acceptsTab != byes) + { + _acceptsTab = byes; + // setStyle(ControlStyles.WANT_TAB_KEY, _acceptsTab); // Do not call here + + // TODO: implement + // onAcceptsTabChanged(EventArgs.empty); + } + } + + /// ditto + final @property bool acceptsTab() // getter + { + return _acceptsTab; + } + + + /// + @property void borderStyle(BorderStyle bs) // setter + { + final switch(bs) + { + case BorderStyle.FIXED_3D: + _style(_style() & ~WS_BORDER); + _exStyle(_exStyle() | WS_EX_CLIENTEDGE); + break; + + case BorderStyle.FIXED_SINGLE: + _exStyle(_exStyle() & ~WS_EX_CLIENTEDGE); + _style(_style() | WS_BORDER); + break; + + case BorderStyle.NONE: + _style(_style() & ~WS_BORDER); + _exStyle(_exStyle() & ~WS_EX_CLIENTEDGE); + break; + } + + if(created) + { + redrawEntire(); + } + } + + /// ditto + @property BorderStyle borderStyle() // getter + { + if(_exStyle() & WS_EX_CLIENTEDGE) + return BorderStyle.FIXED_3D; + else if(_style() & WS_BORDER) + return BorderStyle.FIXED_SINGLE; + return BorderStyle.NONE; + } + + + /// + final @property bool canUndo() // getter + { + if(!created) + return false; + return SendMessageA(handle, EM_CANUNDO, 0, 0) != 0; + } + + + /// + final @property void hideSelection(bool byes) // setter + { + if(byes) + _style(_style() & ~ES_NOHIDESEL); + else + _style(_style() | ES_NOHIDESEL); + } + + /// ditto + final @property bool hideSelection() // getter + { + return (_style() & ES_NOHIDESEL) == 0; + } + + + /// + final @property void lines(Dstring[] lns) // setter + { + Dstring result; + foreach(Dstring s; lns) + { + result ~= s ~ "\r\n"; + } + if(result.length) // Remove last \r\n. + result = result[0 .. $ - 2]; + text = result; + } + + /// ditto + final @property Dstring[] lines() // getter + { + return stringSplitLines(text); + } + + + /// + @property void maxLength(uint len) // setter + { + if(!len) + { + if(multiline) + _lim = 0xFFFFFFFF; + else + _lim = 0x7FFFFFFE; + } + else + { + _lim = len; + } + + if(created) + { + Message m; + m = Message(handle, EM_SETLIMITTEXT, cast(WPARAM)_lim, 0); + prevWndProc(m); + } + } + + /// ditto + @property uint maxLength() // getter + { + if(created) + _lim = cast(uint)SendMessageA(handle, EM_GETLIMITTEXT, 0, 0); + return _lim; + } + + + /// + final uint getLineCount() + { + if(!multiline) + return 1; + + if(created) + { + return cast(uint)SendMessageA(handle, EM_GETLINECOUNT, 0, 0); + } + + Dstring s; + size_t iw = 0; + uint count = 1; + s = text; + for(; iw != s.length; iw++) + { + if('\r' == s[iw]) + { + if(iw + 1 == s.length) + break; + if('\n' == s[iw + 1]) + { + iw++; + count++; + } + } + } + return count; + } + + + /// + final @property void modified(bool byes) // setter + { + if(created) + SendMessageA(handle, EM_SETMODIFY, byes, 0); + } + + /// ditto + final @property bool modified() // getter + { + if(!created) + return false; + return SendMessageA(handle, EM_GETMODIFY, 0, 0) != 0; + } + + + /// + @property void multiline(bool byes) // setter + { + /+ + if(byes) + _style(_style() & ~ES_AUTOHSCROLL | ES_MULTILINE); + else + _style(_style() & ~ES_MULTILINE | ES_AUTOHSCROLL); + +/ + + // TODO: check if correct implementation. + + LONG st; + + if(byes) + { + st = _style() | ES_MULTILINE | ES_AUTOVSCROLL; + + if(_wrap) + st &= ~ES_AUTOHSCROLL; + else + st |= ES_AUTOHSCROLL; + } + else + { + st = _style() & ~(ES_MULTILINE | ES_AUTOVSCROLL); + + // Always H-scroll when single line. + st |= ES_AUTOHSCROLL; + } + + _style(st); + + _crecreate(); + } + + /// ditto + @property bool multiline() // getter + { + return (_style() & ES_MULTILINE) != 0; + } + + + /// + final @property void readOnly(bool byes) // setter + { + if(created) + { + SendMessageA(handle, EM_SETREADONLY, byes, 0); // Should trigger WM_STYLECHANGED. + invalidate(); // ? + } + else + { + if(byes) + _style(_style() | ES_READONLY); + else + _style(_style() & ~ES_READONLY); + } + } + + /// ditto + final @property bool readOnly() // getter + { + return (_style() & ES_READONLY) != 0; + } + + + /// + @property void selectedText(Dstring sel) // setter + { + /+ + if(created) + SendMessageA(handle, EM_REPLACESEL, FALSE, cast(LPARAM)unsafeStringz(sel)); + +/ + + if(created) + { + //dfl.internal.utf.sendMessage(handle, EM_REPLACESEL, FALSE, sel); + dfl.internal.utf.sendMessageUnsafe(handle, EM_REPLACESEL, FALSE, sel); + } + } + + /// ditto + @property Dstring selectedText() // getter + { + /+ + if(created) + { + uint v1, v2; + SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); + if(v1 == v2) + return null; + assert(v2 > v1); + Dstring result = new char[v2 - v1 + 1]; + result[result.length - 1] = 0; + result = result[0 .. result.length - 1]; + result[] = text[v1 .. v2]; + return result; + } + return null; + +/ + + if(created) + return dfl.internal.utf.getSelectedText(handle); + return null; + } + + + /// + @property void selectionLength(uint len) // setter + { + if(created) + { + uint v1, v2; + SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); + v2 = v1 + len; + SendMessageA(handle, EM_SETSEL, v1, v2); + } + } + + /// ditto + // Current selection length, in characters. + // This does not necessarily correspond to the length of chars; some characters use multiple chars. + // An end of line (\r\n) takes up 2 characters. + @property uint selectionLength() // getter + { + if(created) + { + uint v1, v2; + SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); + assert(v2 >= v1); + return v2 - v1; + } + return 0; + } + + + /// + @property void selectionStart(uint pos) // setter + { + if(created) + { + uint v1, v2; + SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); + assert(v2 >= v1); + v2 = pos + (v2 - v1); + SendMessageA(handle, EM_SETSEL, pos, v2); + } + } + + /// ditto + // Current selection starting index, in characters. + // This does not necessarily correspond to the index of chars; some characters use multiple chars. + // An end of line (\r\n) takes up 2 characters. + @property uint selectionStart() // getter + { + if(created) + { + uint v1, v2; + SendMessageA(handle, EM_GETSEL, cast(WPARAM)&v1, cast(LPARAM)&v2); + return v1; + } + return 0; + } + + + /// + // Number of characters in the textbox. + // This does not necessarily correspond to the number of chars; some characters use multiple chars. + // An end of line (\r\n) takes up 2 characters. + // Return may be larger than the amount of characters. + // This is a lot faster than retrieving the text, but retrieving the text is completely accurate. + @property uint textLength() // getter + { + if(!(ctrlStyle & ControlStyles.CACHE_TEXT) && created()) + //return cast(uint)SendMessageA(handle, WM_GETTEXTLENGTH, 0, 0); + return cast(uint)dfl.internal.utf.sendMessage(handle, WM_GETTEXTLENGTH, 0, 0); + return wtext.length.toI32; + } + + + /// + @property final void wordWrap(bool byes) // setter + { + /+ + if(byes) + _style(_style() | ES_AUTOVSCROLL); + else + _style(_style() & ~ES_AUTOVSCROLL); + +/ + + // TODO: check if correct implementation. + + if(_wrap == byes) + return; + + _wrap = byes; + + // Always H-scroll when single line. + if(multiline) + { + if(byes) + { + _style(_style() & ~(ES_AUTOHSCROLL | WS_HSCROLL)); + } + else + { + LONG st; + st = _style(); + + st |= ES_AUTOHSCROLL; + + if(_hscroll) + st |= WS_HSCROLL; + + _style(st); + } + } + + _crecreate(); + } + + /// ditto + final @property bool wordWrap() // getter + { + //return (_style() & ES_AUTOVSCROLL) != 0; + + return _wrap; + } + + + /// + final void appendText(Dstring txt) + { + if(created) + { + selectionStart = textLength; + selectedText = txt; + } + else + { + text = text ~ txt; + } + } + + + /// + final void clear() + { + /+ + // WM_CLEAR only clears the selection ? + if(created) + SendMessageA(handle, WM_CLEAR, 0, 0); + else + wtext = null; + +/ + + text = null; + } + + + /// + final void clearUndo() + { + if(created) + SendMessageA(handle, EM_EMPTYUNDOBUFFER, 0, 0); + } + + + /// + final void copy() + { + if(created) + { + SendMessageA(handle, WM_COPY, 0, 0); + } + else + { + // There's never a selection if the window isn't created; so just empty the clipboard. + + if(!OpenClipboard(null)) + { + debug(APP_PRINT) + cprintf("Unable to OpenClipboard().\n"); + //throw new DflException("Unable to set clipboard data."); + return; + } + EmptyClipboard(); + CloseClipboard(); + } + } + + + /// + final void cut() + { + if(created) + { + SendMessageA(handle, WM_CUT, 0, 0); + } + else + { + // There's never a selection if the window isn't created; so just empty the clipboard. + + if(!OpenClipboard(null)) + { + debug(APP_PRINT) + cprintf("Unable to OpenClipboard().\n"); + //throw new DflException("Unable to set clipboard data."); + return; + } + EmptyClipboard(); + CloseClipboard(); + } + } + + + /// + final void paste() + { + if(created) + { + SendMessageA(handle, WM_PASTE, 0, 0); + } + else + { + // Can't do anything because there's no selection ? + } + } + + + /// + final void scrollToCaret() + { + if(created) + SendMessageA(handle, EM_SCROLLCARET, 0, 0); + } + + + /// + final void select(uint start, uint length) + { + if(created) + SendMessageA(handle, EM_SETSEL, start, start + length); + } + + alias select = Control.select; // Overload. + + + /// + final void selectAll() + { + if(created) + SendMessageA(handle, EM_SETSEL, 0, -1); + } + + + override Dstring toString() + { + return text; // ? + } + + + /// + final void undo() + { + if(created) + SendMessageA(handle, EM_UNDO, 0, 0); + } + + + /+ + override void createHandle() + { + if(isHandleCreated) + return; + + createClassHandle(TEXTBOX_CLASSNAME); + + onHandleCreated(EventArgs.empty); + } + +/ + + + override void createHandle() + { + if(!isHandleCreated) + { + Dstring txt; + txt = wtext; + + super.createHandle(); + + //dfl.internal.utf.setWindowText(hwnd, txt); + text = txt; // So that it can be overridden. + } + } + + + protected override void createParams(ref CreateParams cp) + { + super.createParams(cp); + + cp.className = TEXTBOX_CLASSNAME; + cp.caption = null; // Set in createHandle() to allow larger buffers. + } + + + protected override void onHandleCreated(EventArgs ea) + { + super.onHandleCreated(ea); + + //SendMessageA(hwnd, EM_SETLIMITTEXT, cast(WPARAM)lim, 0); + maxLength = _lim; // Call virtual function. + } + + + private + { + version(DFL_NO_MENUS) + { + } + else + { + void menuUndo(Object sender, EventArgs ea) + { + undo(); + } + + + void menuCut(Object sender, EventArgs ea) + { + cut(); + } + + + void menuCopy(Object sender, EventArgs ea) + { + copy(); + } + + + void menuPaste(Object sender, EventArgs ea) + { + paste(); + } + + + void menuDelete(Object sender, EventArgs ea) + { + // Only clear selection. + SendMessageA(handle, WM_CLEAR, 0, 0); + } + + + void menuSelectAll(Object sender, EventArgs ea) + { + selectAll(); + } + + + bool isClipboardText() + { + if(!OpenClipboard(handle)) + return false; + + bool result; + result = GetClipboardData(CF_TEXT) != null; + + CloseClipboard(); + + return result; + } + + + void menuPopup(Object sender, EventArgs ea) + { + int slen, tlen; + bool issel; + + slen = selectionLength; + tlen = textLength; + issel = slen != 0; + + _miundo.enabled = canUndo; + _micut.enabled = !readOnly() && issel; + _micopy.enabled = issel; + _mipaste.enabled = !readOnly() && isClipboardText(); + _midel.enabled = !readOnly() && issel; + _misel.enabled = tlen != 0 && tlen != slen; + } + + + MenuItem _miundo, _micut, _micopy, _mipaste, _midel, _misel; + } + } + + + this() + { + _initTextBox(); + + wstyle |= WS_TABSTOP | ES_AUTOHSCROLL; + wexstyle |= WS_EX_CLIENTEDGE; + ctrlStyle |= ControlStyles.SELECTABLE; + wclassStyle = textBoxClassStyle; + + version(DFL_NO_MENUS) + { + } + else + { + MenuItem mi; + + cmenu = new ContextMenu; + cmenu.popup ~= &menuPopup; + + _miundo = new MenuItem; + _miundo.text = "&Undo"; + _miundo.click ~= &menuUndo; + _miundo.index = 0; + cmenu.menuItems.add(_miundo); + + mi = new MenuItem; + mi.text = "-"; + mi.index = 1; + cmenu.menuItems.add(mi); + + _micut = new MenuItem; + _micut.text = "Cu&t"; + _micut.click ~= &menuCut; + _micut.index = 2; + cmenu.menuItems.add(_micut); + + _micopy = new MenuItem; + _micopy.text = "&Copy"; + _micopy.click ~= &menuCopy; + _micopy.index = 3; + cmenu.menuItems.add(_micopy); + + _mipaste = new MenuItem; + _mipaste.text = "&Paste"; + _mipaste.click ~= &menuPaste; + _mipaste.index = 4; + cmenu.menuItems.add(_mipaste); + + _midel = new MenuItem; + _midel.text = "&Delete"; + _midel.click ~= &menuDelete; + _midel.index = 5; + cmenu.menuItems.add(_midel); + + mi = new MenuItem; + mi.text = "-"; + mi.index = 6; + cmenu.menuItems.add(mi); + + _misel = new MenuItem; + _misel.text = "Select &All"; + _misel.click ~= &menuSelectAll; + _misel.index = 7; + cmenu.menuItems.add(_misel); + } + } + + + override @property Color backColor() // getter + { + if(Color.empty == backc) + return defaultBackColor; + return backc; + } + + alias backColor = Control.backColor; // Overload. + + + static @property Color defaultBackColor() // getter + { + return Color.systemColor(COLOR_WINDOW); + } + + + override @property Color foreColor() // getter + { + if(Color.empty == forec) + return defaultForeColor; + return forec; + } + + alias foreColor = Control.foreColor; // Overload. + + + static @property Color defaultForeColor() //getter + { + return Color.systemColor(COLOR_WINDOWTEXT); + } + + + override @property Cursor cursor() // getter + { + if(!wcurs) + return _defaultCursor; + return wcurs; + } + + alias cursor = Control.cursor; // Overload. + + + /// + int getFirstCharIndexFromLine(int line) + { + if(!isHandleCreated) + return -1; // ... + if(line < 0) + return -1; + return SendMessageA(hwnd, EM_LINEINDEX, line, 0).toI32; + } + + /// ditto + int getFirstCharIndexOfCurrentLine() + { + if(!isHandleCreated) + return -1; // ... + return SendMessageA(hwnd, EM_LINEINDEX, -1, 0).toI32; + } + + + /// + int getLineFromCharIndex(int charIndex) + { + if(!isHandleCreated) + return -1; // ... + if(charIndex < 0) + return -1; + return SendMessageA(hwnd, EM_LINEFROMCHAR, charIndex, 0).toI32; + } + + + /// + Point getPositionFromCharIndex(int charIndex) + { + if(!isHandleCreated) + return Point(0, 0); // ... + if(charIndex < 0) + return Point(0, 0); + POINT point; + SendMessageA(hwnd, EM_POSFROMCHAR, cast(WPARAM)&point, charIndex); + return Point(point.x, point.y); + } + + /// ditto + int getCharIndexFromPosition(Point pt) + { + if(!isHandleCreated) + return -1; // ... + if(!multiline) + return 0; + auto lresult = SendMessageA(hwnd, EM_CHARFROMPOS, 0, MAKELPARAM(pt.x, pt.y)); + if(-1 == lresult) + return -1; + return cast(int)cast(short)(lresult & 0xFFFF); + } + + + package static @property Cursor _defaultCursor() // getter + { + static Cursor def = null; + + if(!def) + { + synchronized + { + if(!def) + def = new SafeCursor(LoadCursorA(null, IDC_IBEAM)); + } + } + + return def; + } + + + protected override void onReflectedMessage(ref Message m) + { + super.onReflectedMessage(m); + + switch(m.msg) + { + case WM_COMMAND: + switch(HIWORD(m.wParam)) + { + case EN_CHANGE: + onTextChanged(EventArgs.empty); + break; + + default: + } + break; + + /+ + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + /+ + //SetBkColor(cast(HDC)m.wParam, backColor.toRgb()); // ? + SetBkMode(cast(HDC)m.wParam, OPAQUE); // ? + +/ + break; + +/ + + default: + } + } + + + // processCmdKey returns true when keyData is a shortcut key (ctrl+C etc). + protected override bool processCmdKey(ref Message msg, Keys keyData) + { + // First call parent's ProcessCmdKey, since we don't to eat up + // the shortcut key we are not supported in TextBox. + bool returnedValue = super.processCmdKey(msg, keyData); + + // TODO: Implement + + // if (shortcutsEnabled == false && s_shortcutsToDisable !is null) + // { + // foreach (int shortcutValue in s_shortcutsToDisable) + // { + // if (keyData == shortcutValue || + // keyData == (shortcutValue | Keys.SHIFT)) + // { + // return true; + // } + // } + // } + + // // + // // There are a few keys that change the alignment of the text, but that + // // are not ignored by the native control when the ReadOnly property is set. + // // We need to workaround that. + // if (_textBoxFlags[readOnly]) + // { + // int k = keyData; + // if (k == Shortcut.CtrlL // align left + // || k == Shortcut.CtrlR // align right + // || k == Shortcut.CtrlE // align centre + // || k == Shortcut.CtrlJ) + // { // align justified + // return true; + // } + // } + + // if (!ReadOnly && (keyData == (Keys.CONTROL | Keys.BACK) || keyData == (Keys.CONTROL | Keys.SHIFT | Keys.BACK))) + // { + // if (selectionLength != 0) + // { + // SetSelectedTextInternal(string.Empty, clearUndo: false); + // } + // else if (SelectionStart != 0) + // { + // int boundaryStart = ClientUtils.GetWordBoundaryStart(Text, SelectionStart); + // int length = SelectionStart - boundaryStart; + // BeginUpdateInternal(); + // SelectionStart = boundaryStart; + // SelectionLength = length; + // EndUpdateInternal(); + // SetSelectedTextInternal(string.Empty, clearUndo: false); + // } + + // return true; + // } + + return returnedValue; + } + + /// isInputKey returns true when keyData is a regular input key. + // If keyData is input key, then window message is sended to wndProc() + // such as WM_KEYDOWN, WM_KEYUP, WM_CHAR and so on. + protected override bool isInputKey(Keys keyData) + { + if ((keyData & Keys.ALT) != Keys.ALT) + { + // In order to excepted for modifiers, + // do bit mask to extract only key code from key value. + switch (keyData & Keys.KEY_CODE) + { + case Keys.TAB: + // Single-line RichTextBox's want tab characters (see WM_GETDLGCODE), + // so we don't ask it + bool m = multiline; + bool a = _acceptsTab; + bool k = ((keyData & Keys.CONTROL) == 0); + if (m && a) + { + if (k) + return true; // TAB is input key because CONTROL is relesed. + else + return false; // TAB is not input key because CONTROL is pressed. + } + else + { + return false; // TAB is not input key. + } + + case Keys.ESCAPE: + if (multiline) + { + return false; + } + + break; + case Keys.BACK: + // TODO: implement + // if (!ReadOnly) + // { + // return true; + // } + + break; + case Keys.PAGE_UP: + case Keys.PAGE_DOWN: + case Keys.HOME: + case Keys.END: + return true; + default: + // Fall through to super.isInputKey() + } + } + + return super.isInputKey(keyData); + } + + /// Process dialog key (TAB, RETURN, ESC, UP, DOWN, LEFT, RIGHT and so on). + // Returns true when processed, otherwise call super class. + protected override bool processDialogKey(Keys keyData) + { + Keys keyCode = keyData & Keys.KEY_CODE; + + if (keyCode == Keys.TAB && _acceptsTab) + { + if ((keyData & Keys.CONTROL) != 0) + { + // Changes focus because pressed ctrl+TAB in accepts-tab mode. + return super.processDialogKey(keyData); + } + else + { + return true; // processed. + } + } + + // if (keyCode == Keys.TAB && _acceptsTab) && (keyData & Keys.CONTROL) != 0) + // { + // // When this control accepts Tabs, Ctrl-Tab is treated exactly like Tab. + // keyData &= ~Keys.CONTROL; + // } + + return super.processDialogKey(keyData); + } + + + protected override void prevWndProc(ref Message msg) + { + version(DFL_NO_MENUS) + { + // Don't prevent WM_CONTEXTMENU so at least it'll have a default menu. + } + else + { + if(msg.msg == WM_CONTEXTMENU) // Ignore the default context menu. + return; + } + + //msg.result = CallWindowProcA(textBoxPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); + msg.result = dfl.internal.utf.callWindowProc(textBoxPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); + } + + + // WinFormsに合わせて未定義とする + // + // protected override bool processKeyEventArgs(ref Message msg) // package + // { + // switch(msg.msg) + // { + // case WM_KEYDOWN: + // case WM_KEYUP: + // case WM_CHAR: + // if('\t' == msg.wParam) + // { + // // TODO: fix this. This case shouldn't be needed. + // if(_acceptsTab) + // { + // if(super.processKeyEventArgs(msg)) + // return true; // Handled. + // if(WM_KEYDOWN == msg.msg) + // { + // if(multiline) // Only multiline textboxes can have real tabs.. + // { + // //selectedText = "\t"; + // //SendMessageA(handle, EM_REPLACESEL, TRUE, cast(LPARAM)"\t".ptr); // Allow undo. // Crashes DMD 0.161. + // auto str = "\t".ptr; + // SendMessageA(handle, EM_REPLACESEL, TRUE, cast(LPARAM)str); // Allow undo. + // } + // } + // return true; // Handled. + // } + // } + // break; + + // default: + // } + // return super.processKeyEventArgs(msg); + // } + + + protected override void wndProc(ref Message msg) + { + switch(msg.msg) + { + // case WM_KEYDOWN: + // case WM_SYSKEYDOWN: + // case WM_CHAR: + // case WM_SYSCHAR: + // case WM_KEYUP: + // case WM_SYSKEYUP: + // Keys keyCode = cast(Keys)msg.wParam & Keys.KEY_CODE; + + // if (keyCode == Keys.TAB && _acceptsTab) + // { + // if ((keyCode & Keys.CONTROL) != 0) + // { + // // Changes focus because pressed ctrl+TAB in accepts-tab mode. + // return super.wndProc(msg); + // } + // else + // { + // return; // processed. + // } + // } + // break; + + case WM_GETDLGCODE: + // DLGC_WANTARROWS = 1 + // Want VK_LEFT, VK_RIGHT, VK_UP and VK_DOWN in WM_KEYDOWN. + // DLGC_WANTTAB = 2 + // Want VK_TAB in WM_KEYDOWN. + // DLGC_WANTALLKEYS = 4 + // Want VK_RETURN, VK_EXECUTE, VK_ESCAPE and VK_CANCEL in WM_KEYDOWN. + // DLGC_WANTCHARS = 0x80 + // Want WM_CHAR. + + // If this code is commented out, + // do not able to change focus and input TAB char by TAB key. + if(_acceptsTab) + { + msg.result |= DLGC_WANTTAB; + } + else + { + msg.result &= ~(DLGC_WANTTAB | DLGC_WANTALLKEYS); + } + + return; // Do not call super.wndProc() because processing TAB was done. + + default: + } + super.wndProc(msg); + } + + + protected override @property Size defaultSize() // getter + { + return Size(120, 23); // ? + } + + + protected final @property void hscroll(bool byes) // setter + { + _hscroll = byes; + + if(byes && (!_wrap || !multiline)) + _style(_style() | WS_HSCROLL | ES_AUTOHSCROLL); + } + + + protected final @property bool hscroll() // getter + { + return _hscroll; + } + + private: + package uint _lim = 30_000; // Documented as default. + bool _wrap = true; + bool _hscroll; + bool _acceptsTab = false; +}