# Copyright (c) 2023 M.J.N. Corino, The Netherlands # # This software is released under the MIT license. # # Adapted for wxRuby from wxWidgets widgets sample # Copyright (c) 2001 Vadim Zeitlin module Widgets module TextCtrl class WidgetsTextCtrl < Wx::TextCtrl def initialize(parent, id, value, flags) super(parent, id, value, style: flags) evt_left_down :on_left_click end private # Show the result of HitTest() at the mouse position if Alt is pressed. def on_left_click(event) event.skip return unless event.alt_down rc, x, y = hit_test(event.position) case rc when Wx::TE_HT_UNKNOWN x = y = -1 where = 'nowhere near' when Wx::TE_HT_BEFORE where = 'before' when Wx::TE_HT_BELOW where = 'below' when Wx::TE_HT_BEYOND where = 'beyond' when Wx::TE_HT_ON_TEXT where = 'at' else ::Kernel.raise RuntimeError, 'unexpected HitTest() result' end Wx.log_message("Mouse is #{where} (#{x}, #{y})") end end class TextCtrlPage < Widgets::Page module ID include Wx::IDHelper Reset = self.next_id(Widgets::Frame::ID::Last) Set = self.next_id Add = self.next_id Insert = self.next_id Clear = self.next_id Load = self.next_id # StreamRedirector : not supported with wxRuby Password = self.next_id NoVertScrollbar = self.next_id WrapLines = self.next_id Textctrl = self.next_id TextLines_Single = 0 TextLines_Multi = 1 TextLines_Max = 2 WrapStyle_None = 0 WrapStyle_Word = 1 WrapStyle_Char = 2 WrapStyle_Best = 3 WrapStyle_Max = 4 Align_Left = 0 Align_Center = 1 Align_Right = 2 if Wx::PLATFORM == 'WXMSW' TextKind_Plain = 0 TextKind_Rich = 1 TextKind_Rich2 = 2 TextKind_Max = 3 end end attrs = [:text_lines, :password, :readonly, :process_enter, :process_tab, :filename, :no_vert_scrollbar, :wrap_style, :alignment_style] attrs << :text_kind if Wx::PLATFORM == 'WXMSW' defs = [ID::TextLines_Multi, # multiline false, # not password false, # not readonly true, # do process enter false, # do not process Tab false, # not filename false, # don't hide vertical scrollbar ID::WrapStyle_Word, # wrap on word boundaries ID::Align_Left # leading-alignment ] defs << ID::TextKind_Plain if Wx::PLATFORM == 'WXMSW' # plain EDIT control DEFAULTS = Struct.new(*attrs).new(*defs) def initialize(book, images) super(book, images, :text) # init everything if Wx::PLATFORM == 'WXMSW' @radioKind = nil end # WXMSW @radioWrap = @radioAlign = @radioTextLines = nil @chkPassword = @chkReadonly = @chkProcessEnter = @chkProcessTab = @chkFilename = @chkNoVertScrollbar = nil @text = @textPosCur = @textRowCur = @textColCur = @textPosLast = @textLineLast = @textSelFrom = @textSelTo = @textRange = nil @sizerText = nil @posCur = @posLast = @selFrom = @selTo = -2 # not -1 which means "no selection" end Info = Widgets::PageInfo.new(self, 'Text', Widgets::NATIVE_CTRLS | Widgets::EDITABLE_CTRLS) def get_widget @text end def get_text_entry @text end def recreate_widget create_text end # lazy creation of the content def create_content # left pane modes = [ "single line", "multi line", ] sizerLeft = Wx::StaticBoxSizer.new(Wx::VERTICAL, self, '&Set textctrl parameters') sizerLeftBox = sizerLeft.get_static_box @radioTextLines = Wx::RadioBox.new(sizerLeftBox, Wx::ID_ANY, '&Number of lines:', choices: modes, major_dimension: 1, style: Wx::RA_SPECIFY_COLS) sizerLeft.add(@radioTextLines, 0, Wx::GROW | Wx::ALL, 5) sizerLeft.add_spacer(5) @chkPassword = create_check_box_and_add_to_sizer( sizerLeft, '&Password control', ID::Password, sizerLeftBox) @chkReadonly = create_check_box_and_add_to_sizer( sizerLeft, '&Read-only mode', Wx::ID_ANY, sizerLeftBox) @chkProcessEnter = create_check_box_and_add_to_sizer( sizerLeft, 'Process &Enter', Wx::ID_ANY, sizerLeftBox) @chkProcessTab = create_check_box_and_add_to_sizer( sizerLeft, 'Process &Tab', Wx::ID_ANY, sizerLeftBox) @chkFilename = create_check_box_and_add_to_sizer( sizerLeft, '&Filename control', Wx::ID_ANY, sizerLeftBox) @chkNoVertScrollbar = create_check_box_and_add_to_sizer( sizerLeft, 'No &vertical scrollbar', ID::NoVertScrollbar, sizerLeftBox) @chkFilename.disable # not implemented yet sizerLeft.add_spacer(5) wrap = [ 'no wrap', 'word wrap', 'char wrap', 'best wrap', ] @radioWrap = Wx::RadioBox.new(sizerLeftBox, ID::WrapLines, '&Wrap style:', choices: wrap, major_dimension: 1, style: Wx::RA_SPECIFY_COLS) sizerLeft.add(@radioWrap, 0, Wx::GROW | Wx::ALL, 5) halign = %w[left centre right] @radioAlign = Wx::RadioBox.new(sizerLeftBox, Wx::ID_ANY, '&Text alignment', choices: halign, major_dimension: 1) sizerLeft.add(@radioAlign, 0, Wx::GROW | Wx::ALL, 5) if Wx::PLATFORM == 'WXMSW' kinds = [ 'plain edit', 'rich edit', 'rich edit 2.0', ] @radioKind = Wx::RadioBox.new(sizerLeftBox, Wx::ID_ANY, 'Control &kind', choices: kinds, major_dimension: 1, style: Wx::RA_SPECIFY_COLS) sizerLeft.add_spacer(5) sizerLeft.add(@radioKind, 0, Wx::GROW | Wx::ALL, 5) end # WXMSW btn = Wx::Button.new(sizerLeftBox, ID::Reset, '&Reset') sizerLeft.add(2, 2, 0, Wx::GROW | Wx::ALL, 1) # spacer sizerLeft.add(btn, 0, Wx::ALIGN_CENTRE_HORIZONTAL | Wx::ALL, 15) # middle pane sizerMiddleUp = Wx::StaticBoxSizer.new(Wx::VERTICAL, self, '&Change contents:') sizerMiddleUpBox = sizerMiddleUp.get_static_box btn = Wx::Button.new(sizerMiddleUpBox, ID::Set, '&Set text value') sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) btn = Wx::Button.new(sizerMiddleUpBox, ID::Add, '&Append text') sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) btn = Wx::Button.new(sizerMiddleUpBox, ID::Insert, '&Insert text') sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) btn = Wx::Button.new(sizerMiddleUpBox, ID::Load, '&Load file') sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) btn = Wx::Button.new(sizerMiddleUpBox, ID::Clear, '&Clear') sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) # not supported by wxRuby # btn = Wx::Button.new(sizerMiddleUpBox, ID::StreamRedirector, 'St&ream redirection') # sizerMiddleUp.add(btn, 0, Wx::ALL | Wx::GROW, 1) sizerMiddleDown = Wx::StaticBoxSizer.new(Wx::VERTICAL, self, '&Info:') sizerMiddleDownBox = sizerMiddleDown.get_static_box @textPosCur = create_info_text(sizerMiddleDownBox) @textRowCur = create_info_text(sizerMiddleDownBox) @textColCur = create_info_text(sizerMiddleDownBox) sizerRow = Wx::HBoxSizer.new sizerRow.add(create_text_with_label_sizer('Current pos:', @textPosCur, '', nil, sizerMiddleDownBox), 0, Wx::RIGHT, 5) sizerRow.add(create_text_with_label_sizer('Col:', @textColCur, '', nil, sizerMiddleDownBox), 0, Wx::LEFT | Wx::RIGHT, 5) sizerRow.add(create_text_with_label_sizer('Row:', @textRowCur, '', nil, sizerMiddleDownBox ), 0, Wx::LEFT, 5) sizerMiddleDown.add(sizerRow, 0, Wx::ALL, 5) @textLineLast = create_info_text(sizerMiddleDownBox) @textPosLast = create_info_text(sizerMiddleDownBox) sizerMiddleDown.add(create_text_with_label_sizer('Number of lines:', @textLineLast, 'Last position:', @textPosLast, sizerMiddleDownBox), 0, Wx::ALL, 5) @textSelFrom = create_info_text(sizerMiddleDownBox) @textSelTo = create_info_text(sizerMiddleDownBox) sizerMiddleDown.add(create_text_with_label_sizer('Selection: from', @textSelFrom, 'to', @textSelTo, sizerMiddleDownBox), 0, Wx::ALL, 5) @textRange = Wx::TextCtrl.new(sizerMiddleDownBox, Wx::ID_ANY, style: Wx::TE_READONLY) sizerMiddleDown.add(create_text_with_label_sizer('Range 10..20:', @textRange, '', nil, sizerMiddleDownBox), 0, Wx::ALL, 5) sizerMiddleDown.add(Wx::StaticText.new(sizerMiddleDownBox, Wx::ID_ANY, 'Alt-click in the text to see HitTest() result'), Wx::SizerFlags.new.border) sizerMiddle = Wx::VBoxSizer.new sizerMiddle.add(sizerMiddleUp, 0, Wx::GROW) sizerMiddle.add(sizerMiddleDown, 1, Wx::GROW | Wx::TOP, 5) # right pane @sizerText = Wx::StaticBoxSizer.new(Wx::HORIZONTAL, self, '&Text:') reset create_text @sizerText.set_min_size(150, 0) # the 3 panes panes compose the upper part of the window sizerTop = Wx::HBoxSizer.new sizerTop.add(sizerLeft, 0, Wx::GROW | (Wx::ALL & ~Wx::LEFT), 10) sizerTop.add(sizerMiddle, 0, Wx::GROW | Wx::ALL, 10) sizerTop.add(@sizerText, 1, Wx::GROW | (Wx::ALL & ~Wx::RIGHT), 10) set_sizer(sizerTop) # connect event handlers evt_idle(:on_idle) evt_button(ID::Reset, :on_button_reset) # not supported by wxRuby # evt_button(ID::StreamRedirector, :on_stream_redirector) evt_button(ID::Clear, :on_button_clear) evt_button(ID::Set, :on_button_set) evt_button(ID::Add, :on_button_add) evt_button(ID::Insert, :on_button_insert) evt_button(ID::Load, :on_button_load) evt_update_ui(ID::Clear, :on_update_ui_clear_button) evt_update_ui(ID::Password, :on_update_ui_password_checkbox) evt_update_ui(ID::NoVertScrollbar, :on_update_ui_no_vert_scrollbar_checkbox) evt_update_ui(ID::WrapLines, :on_update_ui_wrap_lines_radiobox) evt_update_ui(ID::Reset, :on_update_ui_reset_button) evt_text(ID::Textctrl, :on_text) evt_text_enter(ID::Textctrl, :on_text_enter) evt_text_paste(ID::Textctrl, :on_text_pasted) evt_checkbox(Wx::ID_ANY, :on_check_or_radio_box) evt_radiobox(Wx::ID_ANY, :on_check_or_radio_box) end protected class << self def s_max_width(v=nil) @s_max_width = v if v @s_max_width ||= 0 end end # create an info text control def create_info_text(parent) if self.class.s_max_width == 0 # calc it once only sz = get_text_extent("9999999") self.class.s_max_width(sz.width) end Wx::TextCtrl.new(parent, size: [self.class.s_max_width, Wx::DEFAULT_COORD], style: Wx::TE_READONLY) end # create a horz sizer holding a static text and this text control def create_text_with_label_sizer(label, text, label2 = '', text2 = nil, statBoxParent = nil) sizerRow = Wx::HBoxSizer.new sizerRow.add(Wx::StaticText.new(statBoxParent ? statBoxParent : self, Wx::ID_ANY, label), 0, Wx::ALIGN_CENTRE_VERTICAL | Wx::RIGHT, 5) sizerRow.add(text, 0, Wx::ALIGN_CENTRE_VERTICAL) if text2 sizerRow.add(Wx::StaticText.new(statBoxParent ? statBoxParent : self, Wx::ID_ANY, label2), 0, Wx::ALIGN_CENTRE_VERTICAL | Wx::LEFT | Wx::RIGHT, 5) sizerRow.add(text2, 0, Wx::ALIGN_CENTRE_VERTICAL) end sizerRow end # event handlers def on_button_reset(_event) reset create_text end def on_button_set(_event) @text.set_value(@text.get_window_style.allbits?(Wx::TE_MULTILINE) ? "Here,\nthere and\neverywhere" : "Yellow submarine") @text.set_focus end def on_button_add(_event) if @text.get_window_style.allbits?(Wx::TE_MULTILINE) @text.append_text("We all live in a\n") end @text.append_text("Yellow submarine") end def on_button_insert(_event) @text.write_text('Is there anybody going to listen to my story') @text.write_text("\nall about the girl who came to stay") if @text.get_window_style.allbits?(Wx::TE_MULTILINE) end def on_button_clear(_event) @text.clear @text.set_focus end def on_button_load(_event) tm_start = Time.now unless @text.load_file(__FILE__) # this is not supposed to happen ... Wx.log_error('Error loading file.') else elapsed = Time.now - tm_start Wx.log_message("Loaded file '#{__FILE__}' in #{elapsed}s") end end # unsupported by wxRuby # def on_stream_redirector(event) # end def on_text(event) return unless Widgets::Page.is_using_log_window # Replace middle of long text with ellipsis just to avoid filling up the # log control with too much unnecessary stuff. log_str = Wx::ClientDC.draw_on(self) do |dc| Wx::Control.ellipsize(event.string, dc, Wx::ELLIPSIZE_MIDDLE, get_text_extent('W').x * 100) end Wx.log_message("Text control value changed (now '%s')", log_str) end def on_text_enter(event) Wx.log_message("Text entered: '#{event.string}'") event.skip end def on_text_pasted(event) Wx.log_message('Text pasted from clipboard.') event.skip end def on_check_or_radio_box(event) if %w[WXMSW WXGTK WXOSX].include?(Wx::PLATFORM) && event.get_event_object == @radioAlign # We should be able to change text alignment # dynamically, without recreating the control. flags = @text.get_window_style flags &= ~(Wx::TE_LEFT|Wx::TE_CENTER|Wx::TE_RIGHT) case event.selection when ID::Align_Left flags |= Wx::TE_LEFT when ID::Align_Center flags |= Wx::TE_CENTER when ID::Align_Right flags |= Wx::TE_RIGHT else ::Kernel.raise RuntimeError, 'unexpected alignment style radio box selection' end @text.set_window_style(flags) @text.refresh flags = @text.get_window_style Wx.log_message("Text alignment: %s", (flags.allbits?(Wx::TE_RIGHT) ? 'Right' : (flags.allbits?(Wx::TE_CENTER) ? 'Center' : 'Left'))) else create_text end end def on_update_ui_clear_button(event) event.enable(!@text.value.empty?) end def on_update_ui_password_checkbox(event) # can't put multiline control in password mode event.enable(is_single_line) end def on_update_ui_no_vert_scrollbar_checkbox(event) # Vertical scrollbar creation can be blocked only in multiline control event.enable(!is_single_line) end def on_update_ui_wrap_lines_radiobox(event) event.enable(!is_single_line) end def on_update_ui_reset_button(event) event.enable( (@radioTextLines.selection != DEFAULTS.text_lines) || (Wx::PLATFORM == 'WXMSW' && @radioKind.selection != DEFAULTS.text_kind) || (@chkPassword.value != DEFAULTS.password) || (@chkReadonly.value != DEFAULTS.readonly) || (@chkProcessEnter.value != DEFAULTS.process_enter) || (@chkProcessTab.value != DEFAULTS.process_tab) || (@chkFilename.value != DEFAULTS.filename) || (@chkNoVertScrollbar.value != DEFAULTS.no_vert_scrollbar) || (@radioWrap.selection != DEFAULTS.wrap_style) ) end def on_idle(_event) # update all info texts if @textPosCur posCur = @text.get_insertion_point if posCur != @posCur @textPosCur.clear @textRowCur.clear @textColCur.clear col, row = @text.position_to_xy(posCur) @textPosCur << posCur @textRowCur << row @textColCur << col @posCur = posCur end end if @textPosLast posLast = @text.get_last_position if posLast != @posLast @textPosLast.clear @textPosLast << posLast @posLast = posLast end end if @textLineLast @textLineLast.set_value(@text.get_number_of_lines.to_s) end if @textSelFrom && @textSelTo selFrom, selTo = @text.get_selection if selFrom != @selFrom @textSelFrom.clear @textSelFrom << selFrom @selFrom = selFrom end if selTo != @selTo @textSelTo.clear @textSelTo << selTo @selTo = selTo end end if @textRange range = @text.get_range(10, 20) if range != @range10_20 @range10_20 = range @textRange.value = range end end end # reset the textctrl parameters def reset @radioTextLines.set_selection(DEFAULTS.text_lines) @chkPassword.set_value(DEFAULTS.password) @chkReadonly.set_value(DEFAULTS.readonly) @chkProcessEnter.set_value(DEFAULTS.process_enter) @chkProcessTab.set_value(DEFAULTS.process_tab) @chkFilename.set_value(DEFAULTS.filename) @chkNoVertScrollbar.set_value(DEFAULTS.no_vert_scrollbar) @radioWrap.set_selection(DEFAULTS.wrap_style) @radioAlign.set_selection(DEFAULTS.alignment_style) if Wx::PLATFORM == 'WXMSW' @radioKind.set_selection(DEFAULTS.text_kind) end # WXMSW end # (re)create the textctrl def create_text flags = get_attrs.default_flags case @radioTextLines.get_selection when ID::TextLines_Single # nothing when ID::TextLines_Multi flags |= Wx::TE_MULTILINE @chkPassword.set_value(false) else ::Kernel.raise RuntimeError, 'unexpected lines radio box selection' end flags |= Wx::TE_PASSWORD if @chkPassword.value flags |= Wx::TE_READONLY if @chkReadonly.value flags |= Wx::TE_PROCESS_ENTER if @chkProcessEnter.value flags |= Wx::TE_PROCESS_TAB if @chkProcessTab.value flags |= Wx::TE_NO_VSCROLL if @chkNoVertScrollbar.value case @radioWrap.get_selection when ID::WrapStyle_None flags |= Wx::TE_DONTWRAP # same as wxHSCROLL when ID::WrapStyle_Word flags |= Wx::TE_WORDWRAP when ID::WrapStyle_Char flags |= Wx::TE_CHARWRAP when ID::WrapStyle_Best # this is default but use symbolic file name for consistency flags |= Wx::TE_BESTWRAP else ::Kernel.raise RuntimeError, 'unexpected wrap style radio box selection' end case @radioAlign.selection when ID::Align_Left flags |= Wx::TE_LEFT when ID::Align_Center flags |= Wx::TE_CENTER when ID::Align_Right flags |= Wx::TE_RIGHT else ::Kernel.raise RuntimeError, 'unexpected alignment style radio box selection' end if Wx::PLATFORM == 'WXMSW' case @radioKind.selection when ID::TextKind_Plain # nothing when ID::TextKind_Rich flags |= Wx::TE_RICH when ID::TextKind_Rich2 flags |= Wx::TE_RICH2 else ::Kernel.raise RuntimeError, 'unexpected kind radio box selection' end end # WXMSW if @text valueOld = @text.value @sizerText.detach(@text) @text.destroy else valueOld = 'Hello, Universe!' end @text = WidgetsTextCtrl.new(@sizerText.get_static_box, ID::Textctrl, valueOld, flags) # TODO # if @chkFilename.value # end @sizerText.add(@text, 1, Wx::ALL | (flags.allbits?(Wx::TE_MULTILINE) ? Wx::GROW : Wx::ALIGN_TOP), 5) @sizerText.layout end # is the control currently single line? def is_single_line @radioTextLines.get_selection == ID::TextLines_Single end end end end