Notebook word make clickable

Hello,
I’m trying to figure out how to make a certain word string clickable from a notebook page.
It’s currently setup as follows: Panel -> notebook -> notebookpage(“panel”) -> which contains a list of sentences stored using TextCtrl.
The goal is to have certain words from those different sentences made clickable, so can catch that click event and do some stuff (like, launch another program).
I did some digging but still not sure what would be the approach here.
Your help is greatly appreciated.
Thank you

You would use one of the button controls, or else a html ctrl.

Karsten

Hi Karsten,
Thank you for you reply. Could you please elaborate on your answer if possible, maybe you have an snippet example?
Having the structure above and a list of sentences, how would I use the button control on certain words ?
Thank you

You would have to split the text appropriately, and
intersperse textctrls and buttons as needed, perhaps using a
flowsizer.

Karsten

Hi Karsten,
This text data can also be rather large files. Would this be feasible from performance perspective as well?
Can you reference a good example for flowsizer and perhaps an example that intersperses txtctrls and buttons?
Thank you

Wouldn’t it be simpler to just grab the word under the mouse pointer and do a normal python search of your word list to determine if special action should be taken?

Inspired by a similar issue in Stack Overflow, I wrote a word-clicking program that might give some helpful hints:

import wx

sample_text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
eprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum."""

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 'Word Clicker')
        pnl = wx.Panel(self, wx.ID_ANY)
        lbl = wx.StaticText(pnl, wx.ID_ANY, "Source Text")
        self.richText = txt = wx.TextCtrl(
            pnl,
            wx.ID_ANY,
            sample_text,
            style=wx.TE_MULTILINE | wx.TE_RICH2 | wx.TE_READONLY,
        )
        self.found = fnd = wx.StaticText(pnl, wx.ID_ANY, "<result goes here>")
        txt.Bind(wx.EVT_LEFT_DOWN, self.OnGetWord)
        txt.SetInsertionPoint(0)
        txt.SetCursor(wx.Cursor(wx.CURSOR_HAND))
        szr = wx.BoxSizer(wx.VERTICAL)
        szr.AddMany([(lbl,0),(txt,1,wx.EXPAND), (fnd,0,wx.EXPAND)])
        pnl.SetSizerAndFit(szr)


    def OnGetWord(self, event):
        xy_pos = event.GetPosition()
        _, word_pos = self.richText.HitTestPos(xy_pos)
        search_text = self.richText.GetValue()
        left_pos = right_pos = word_pos
        if word_pos < len(search_text) and search_text[word_pos].isalnum():
            try:
                while left_pos >= 0 and search_text[left_pos].isalnum():
                    left_pos -= 1
                while right_pos <= len(search_text) and search_text[right_pos].isalnum():
                    right_pos += 1
                found_word = search_text[left_pos + 1 : right_pos]
                print(f"Found: '{found_word}'")
            except Exception as e:
                found_word = "<" + str(e) + ">"
        else:
            found_word = "<Not On Word>"
        self.found.SetLabel(found_word)

if __name__ == '__main__':
    app = wx.App()
    MyFrame().Show()
    app.MainLoop()

1 Like

Hello Rufus,
Thank you very much for taking the time to answer.
This is great solution and it works.
Given the sample_text above would you know if it’s possible to highlight | underline certain words that I would know in advance which ones need to be emphasized? For ex. I know I want to highlight/underline the words ‘labore’ and ‘fugiat’?
Thank you

I am sure, because it is a rich text box, you can format/highlight words in advance. Unfortunately I don’t have the experience with Rich Text Boxes to know how to do that. I might look into it for academic reasons when I have some downtime though. Also, if you figured out how to do it yourself since the last post, you could post your solution back here, and it might help others with similar issues.
\

Okay, I had some downtime. Had to make some changes to use an actual rich text control. There were also some changes in method names. Here’s some new code that allows you to pre-highlight target words in your text box.

import wx
import wx.richtext
import re
from collections import Counter

sample_text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
eprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum."""

target_words = [
    'labore',
    'fugiat'
]

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 'Word Clicker')
        pnl = wx.Panel(self, wx.ID_ANY)
        lbl = wx.StaticText(pnl, wx.ID_ANY, "Source Text")
        self.richText = txt = wx.richtext.RichTextCtrl(
            pnl,
            wx.ID_ANY,
            sample_text,
            style=wx.TE_MULTILINE | wx.TE_RICH2 | wx.TE_READONLY,
        )
        # fill text highlighting keywords
        txt.Clear()
        splits = re.split("(\w+)",sample_text) # separate words and non-words
        for word in splits:
            if word.lower() in target_words:
                txt.BeginBold()
                txt.BeginTextColour(wx.RED)
                txt.BeginUnderline()
                txt.WriteText(word)                
                txt.EndUnderline()
                txt.EndTextColour()
                txt.EndBold()
            else:
                txt.WriteText(word) # write with no highlight
        
        self.found = fnd = wx.StaticText(pnl, wx.ID_ANY, "<result goes here>")
        txt.Bind(wx.EVT_LEFT_DOWN, self.OnGetWord)
        txt.SetInsertionPoint(0)
        txt.SetTextCursor(wx.Cursor(wx.CURSOR_HAND))
        szr = wx.BoxSizer(wx.VERTICAL)
        szr.AddMany([(lbl,0),(txt,1,wx.EXPAND), (fnd,0,wx.EXPAND)])
        pnl.SetSizerAndFit(szr)


    def OnGetWord(self, event):
        xy_pos = event.GetPosition()
        _, word_pos = self.richText.HitTest(xy_pos)
        search_text = self.richText.GetValue()
        
        # attempt to use select word
        if self.richText.SelectWord(word_pos):
            found_word = self.richText.GetStringSelection()
            print(f"Found: '{found_word}'")
        else:
            found_word = "<Not On Word>"                
            
        self.found.SetLabel(found_word)

if __name__ == '__main__':
    app = wx.App()
    MyFrame().Show()
    app.MainLoop()
    

Hi,

This is another way to use stc.StyledTextCtrl hotspot.

from six.moves import builtins
import keyword
import wx
from wx import stc
from wx.py.editwindow import EditWindow

class TestEditor(EditWindow):
    def __init__(self, *args, **kwargs):
        EditWindow.__init__(self, *args, **kwargs)
        
        self.StyleClearAll()
        
        ## lexer-base style
        self.SetLexer(stc.STC_LEX_PYTHON)
        self.SetKeyWords(0, ' '.join(keyword.kwlist))
        self.SetKeyWords(1, ' '.join(builtins.__dict__) + " she shells")
        
        ## we use P_WORD2+ as hotspot
        self.StyleSetHotSpot(stc.STC_P_WORD2, True)
        self.StyleSetSpec(stc.STC_P_WORD2, "fore:orange,back:yellow,bold")
        
        self.Bind(stc.EVT_STC_HOTSPOT_CLICK, self.OnHotspot)
    
    def OnHotspot(self, evt):
        if self.GetStyleAt(evt.Position) == stc.STC_P_WORD2:
            p = self.WordStartPosition(evt.Position, True)
            q = self.WordEndPosition(evt.Position, True)
            print(self.GetTextRange(p, q))

samplePhrase = """
She sells sea shells by the seashore.
The shells that she sells are sea shells I'm sure.
So if she sells sea shells on the seashore,
I'm sure that the shells are seashore shells.
"""

if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None)
    ed = TestEditor(frame)
    ed.Text = samplePhrase
    frame.Show()
    app.MainLoop()

Yet another way to use stc.StyledTextCtrl indicator.

The hotspot is lexer-base and the style is limited, but the indicator is context-base and can use many style and hover effect (hover is wx 4.1.0 or later)

import wx
from wx import stc
from wx.py.editwindow import EditWindow

class TestEditor(EditWindow):
    def __init__(self, *args, **kwargs):
        EditWindow.__init__(self, *args, **kwargs)
        
        self.StyleClearAll()
        
        ## context-base style
        if wx.VERSION < (4,1,0):
            self.IndicatorSetStyle(0, stc.STC_INDIC_PLAIN)
            self.IndicatorSetStyle(1, stc.STC_INDIC_CONTAINER)
        else:
            self.IndicatorSetStyle(0, stc.STC_INDIC_TEXTFORE)
            self.IndicatorSetStyle(1, stc.STC_INDIC_ROUNDBOX)
        self.IndicatorSetForeground(0, "red")
        self.IndicatorSetForeground(1, "yellow")
        
        if wx.VERSION >= (4,1,0):
            self.IndicatorSetHoverStyle(1, stc.STC_INDIC_ROUNDBOX)
            self.IndicatorSetHoverForeground(1, "blue")
        
        self.Bind(stc.EVT_STC_INDICATOR_CLICK, self.OnIndicator)
    
    def OnIndicator(self, evt):
        if self.IndicatorValue == 1:
            p = self.IndicatorStart(1, evt.Position)
            q = self.IndicatorEnd(1, evt.Position)
            print(self.GetTextRange(p, q))
    
    def FilterText(self, text):
        if not text:
            for i in range(2):
                self.SetIndicatorCurrent(i)
                self.IndicatorClearRange(0, self.TextLength)
            return
        word = text.encode() # for multi-byte string
        raw = self.TextRaw # for multi-byte string
        lw = len(word)
        pos = -1
        while 1:
            pos = raw.find(word, pos+1)
            if pos < 0:
                break
            for i in range(2):
                self.SetIndicatorCurrent(i)
                self.IndicatorFillRange(pos, lw)

samplePhrase = """
She sells sea shells by the seashore.
The shells that she sells are sea shells I'm sure.
So if she sells sea shells on the seashore,
I'm sure that the shells are seashore shells.
"""

if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None)
    ed = TestEditor(frame)
    ed.Text = samplePhrase
    ed.FilterText("shells")
    ed.FilterText("she")
    frame.Show()
    app.MainLoop()

Clipboard04