KiCad PCB EDA Suite
Loading...
Searching...
No Matches
html_message_box.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2014 Jean-Pierre Charras, jp.charras at wanadoo.fr
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <wx/clipbrd.h>
22#include <wx/log.h>
23#include <wx/textctrl.h>
24#include <wx/uri.h>
25#include <wx/panel.h>
26#include <wx/sizer.h>
27#include <wx/button.h>
28#include <wx/stattext.h>
29#include <algorithm>
30#include <string_utils.h>
32#include <build_version.h>
33
34
35HTML_MESSAGE_BOX::HTML_MESSAGE_BOX( wxWindow* aParent, const wxString& aTitle,
36 const wxPoint& aPosition, const wxSize& aSize ) :
37 DIALOG_DISPLAY_HTML_TEXT_BASE( aParent, wxID_ANY, aTitle, aPosition, aSize ),
39{
40 m_htmlWindow->SetLayoutDirection( wxLayout_LeftToRight );
41 ListClear();
42
43 m_searchPanel = new wxPanel( this );
44 wxBoxSizer* searchSizer = new wxBoxSizer( wxHORIZONTAL );
45 m_matchCount = new wxStaticText( m_searchPanel, wxID_ANY, wxEmptyString );
46 m_searchCtrl = new wxTextCtrl( m_searchPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER );
47 m_prevBtn = new wxButton( m_searchPanel, wxID_ANY, wxS( "∧" ), wxDefaultPosition, wxDefaultSize, wxBORDER_NONE );
48 m_nextBtn = new wxButton( m_searchPanel, wxID_ANY, wxS( "∨" ), wxDefaultPosition, wxDefaultSize, wxBORDER_NONE );
49
50 // Set minimum size for buttons to make them thinner
51 m_prevBtn->SetMinSize( wxSize( 25, -1 ) );
52 m_nextBtn->SetMinSize( wxSize( 25, -1 ) );
53
54 // Set minimum width for match count to ensure it's visible
55 m_matchCount->SetMinSize( wxSize( 60, -1 ) );
56
57 searchSizer->Add( m_matchCount, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
58 searchSizer->Add( m_searchCtrl, 1, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
59 searchSizer->Add( m_prevBtn, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 2 );
60 searchSizer->Add( m_nextBtn, 0, wxALIGN_CENTER_VERTICAL );
61 m_searchPanel->SetSizer( searchSizer );
62 m_searchPanel->Hide();
63 GetSizer()->Insert( 0, m_searchPanel, 0, wxALIGN_RIGHT|wxTOP|wxRIGHT, 5 );
64
65 m_searchCtrl->Bind( wxEVT_TEXT, &HTML_MESSAGE_BOX::OnSearchText, this );
66 m_searchCtrl->Bind( wxEVT_TEXT_ENTER, &HTML_MESSAGE_BOX::OnNext, this );
67 m_prevBtn->Bind( wxEVT_BUTTON, &HTML_MESSAGE_BOX::OnPrev, this );
68 m_nextBtn->Bind( wxEVT_BUTTON, &HTML_MESSAGE_BOX::OnNext, this );
69
70 // Gives a default logical size (the actual size depends on the display definition)
71 if( aSize != wxDefaultSize )
72 setSizeInDU( aSize.x, aSize.y );
73
74 Center();
75
77
78 reload();
79
80 Bind( wxEVT_SYS_COLOUR_CHANGED,
81 wxSysColourChangedEventHandler( HTML_MESSAGE_BOX::onThemeChanged ), this );
82}
83
84
86{
87 // Prevent wxWidgets bug which fails to release when closing the window on an <esc>.
88 if( m_htmlWindow->HasCapture() )
89 m_htmlWindow->ReleaseMouse();
90}
91
92
94{
95 m_htmlWindow->SetPage( m_source );
96}
97
98
99void HTML_MESSAGE_BOX::onThemeChanged( wxSysColourChangedEvent &aEvent )
100{
101 reload();
102
103 aEvent.Skip();
104}
105
106
108{
109 m_source.clear();
110 reload();
111}
112
113
114void HTML_MESSAGE_BOX::ListSet( const wxString& aList )
115{
116 wxArrayString strings_list;
117 wxStringSplit( aList, strings_list, wxChar( '\n' ) );
118
119 wxString msg = wxT( "<ul>" );
120
121 for ( unsigned ii = 0; ii < strings_list.GetCount(); ii++ )
122 {
123 msg += wxT( "<li>" );
124 msg += strings_list.Item( ii ) + wxT( "</li>" );
125 }
126
127 msg += wxT( "</ul>" );
128
129 m_source += msg;
130 reload();
131}
132
133
134void HTML_MESSAGE_BOX::ListSet( const wxArrayString& aList )
135{
136 wxString msg = wxT( "<ul>" );
137
138 for( unsigned ii = 0; ii < aList.GetCount(); ii++ )
139 {
140 msg += wxT( "<li>" );
141 msg += aList.Item( ii ) + wxT( "</li>" );
142 }
143
144 msg += wxT( "</ul>" );
145
146 m_source += msg;
147 reload();
148}
149
150
151void HTML_MESSAGE_BOX::MessageSet( const wxString& message )
152{
153 wxString message_value = wxString::Format( wxT( "<b>%s</b><br>" ), message );
154
155 m_source += message_value;
156 reload();
157}
158
159
160void HTML_MESSAGE_BOX::AddHTML_Text( const wxString& message )
161{
162 m_source += message;
163 reload();
164}
165
166
168{
169 reload();
170
171 m_sdbSizer1->Show( false );
172 Layout();
173
174 Show( true );
175}
176
177
178void HTML_MESSAGE_BOX::OnHTMLLinkClicked( wxHtmlLinkEvent& event )
179{
180 wxString href = event.GetLinkInfo().GetHref();
181
182 if( href.StartsWith( wxS( "https://go.kicad.org/docs" ) ) )
183 {
184 href.Replace( wxS( "GetMajorMinorVersion" ), GetMajorMinorVersion() );
185 }
186
187 wxURI uri( href );
188 wxLaunchDefaultBrowser( uri.BuildURI() );
189}
190
191
192void HTML_MESSAGE_BOX::OnCharHook( wxKeyEvent& aEvent )
193{
194 if( m_searchPanel->IsShown() )
195 {
196 if( aEvent.GetKeyCode() == WXK_ESCAPE )
197 {
199 return;
200 }
201 else if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
202 {
203 wxCommandEvent evt;
204 OnNext( evt );
205 return;
206 }
207 else if( aEvent.GetKeyCode() == WXK_BACK )
208 {
209 long from, to;
210 m_searchCtrl->GetSelection( &from, &to );
211
212 if( from == to )
213 m_searchCtrl->Remove( std::max( 0L, from - 1 ), from );
214 else
215 m_searchCtrl->Remove( from, to );
216
217 return;
218 }
219 else if( !aEvent.HasModifiers() && wxIsprint( aEvent.GetUnicodeKey() ) )
220 {
221 m_searchCtrl->AppendText( wxString( (wxChar) aEvent.GetUnicodeKey() ) );
222 return;
223 }
224 }
225 else if( !aEvent.HasModifiers() && wxIsprint( aEvent.GetUnicodeKey() ) )
226 {
228 m_searchCtrl->SetValue( wxString( (wxChar) aEvent.GetUnicodeKey() ) );
229 m_currentMatch = 0;
230 updateSearch();
231 return;
232 }
233
234 if( aEvent.GetKeyCode() == WXK_ESCAPE )
235 {
236 wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
237 return;
238 }
239 else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'A' )
240 {
241 m_htmlWindow->SelectAll();
242 return;
243 }
244 else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'C' )
245 {
246 wxLogNull doNotLog; // disable logging of failed clipboard actions
247
248 if( wxTheClipboard->Open() )
249 {
250 wxTheClipboard->SetData( new wxTextDataObject( m_htmlWindow->SelectionToText() ) );
251 wxTheClipboard->Flush(); // Allow data to be available after closing KiCad
252 wxTheClipboard->Close();
253 }
254
255 return;
256 }
257
258 aEvent.Skip();
259}
260
261void HTML_MESSAGE_BOX::OnSearchText( wxCommandEvent& aEvent )
262{
263 m_currentMatch = 0;
264 updateSearch();
265}
266
267void HTML_MESSAGE_BOX::OnNext( wxCommandEvent& aEvent )
268{
269 if( !m_matchPos.empty() )
270 {
271 m_currentMatch = ( m_currentMatch + 1 ) % m_matchPos.size();
272 updateSearch();
273 }
274}
275
276void HTML_MESSAGE_BOX::OnPrev( wxCommandEvent& aEvent )
277{
278 if( !m_matchPos.empty() )
279 {
280 m_currentMatch = ( m_currentMatch + m_matchPos.size() - 1 ) % m_matchPos.size();
281 updateSearch();
282 }
283}
284
286{
287 if( !m_searchPanel->IsShown() )
288 {
290 m_searchPanel->Show();
291 Layout();
292 m_searchCtrl->SetFocus();
293 }
294}
295
297{
298 if( m_searchPanel->IsShown() )
299 {
300 m_searchPanel->Hide();
301 Layout();
303 reload();
304
305 // Refocus on the main HTML window so user can press Escape again to close the dialog
306 m_htmlWindow->SetFocus();
307 }
308}
309
311{
312 wxString term = m_searchCtrl->GetValue();
313
314 if( term.IsEmpty() )
315 {
317 reload();
318 m_matchPos.clear();
319 m_matchCount->SetLabel( wxEmptyString );
320 return;
321 }
322
323 m_matchPos.clear();
324
325 // Search only in text content, not in HTML tags
326 wxString termLower = term.Lower();
327 size_t pos = 0;
328 bool insideTag = false;
329
330 while( pos < m_originalSource.length() )
331 {
332 wxChar ch = m_originalSource[pos];
333
334 if( ch == '<' )
335 {
336 insideTag = true;
337 }
338 else if( ch == '>' )
339 {
340 insideTag = false;
341 pos++;
342 continue;
343 }
344
345 // Only search for matches when we're not inside an HTML tag
346 if( !insideTag )
347 {
348 // Check if we have a match starting at this position
349 if( pos + termLower.length() <= m_originalSource.length() )
350 {
351 wxString candidate = m_originalSource.Mid( pos, termLower.length() ).Lower();
352 if( candidate == termLower )
353 {
354 // Verify that this match doesn't span into an HTML tag
355 bool validMatch = true;
356 for( size_t i = 0; i < termLower.length(); i++ )
357 {
358 if( pos + i < m_originalSource.length() && m_originalSource[pos + i] == '<' )
359 {
360 validMatch = false;
361 break;
362 }
363 }
364
365 if( validMatch )
366 {
367 m_matchPos.push_back( pos );
368 }
369 }
370 }
371 }
372
373 pos++;
374 }
375
376 if( m_matchPos.empty() )
377 {
379 reload();
380 m_matchCount->SetLabel( wxS( "0/0" ) );
381 return;
382 }
383
384 if( m_currentMatch >= (int) m_matchPos.size() )
385 m_currentMatch = 0;
386
387 wxString out;
388 size_t start = 0;
389
390 for( size_t i = 0; i < m_matchPos.size(); ++i )
391 {
392 size_t idx = m_matchPos[i];
393 out += m_originalSource.Mid( start, idx - start );
394 wxString matchStr = m_originalSource.Mid( idx, term.length() );
395
396 // HTML-escape the match string to prevent HTML parsing issues
397 wxString escapedMatchStr = matchStr;
398 escapedMatchStr.Replace( wxS( "&" ), wxS( "&amp;" ) );
399 escapedMatchStr.Replace( wxS( "<" ), wxS( "&lt;" ) );
400 escapedMatchStr.Replace( wxS( ">" ), wxS( "&gt;" ) );
401 escapedMatchStr.Replace( wxS( "\"" ), wxS( "&quot;" ) );
402
403 if( (int) i == m_currentMatch )
404 {
405 // Use a unique anchor name for each search to avoid conflicts
406 wxString anchorName = wxString::Format( wxS( "kicad_search_%d" ), m_currentMatch );
407 out += wxString::Format( wxS( "<a name=\"%s\"></a><span style=\"background-color:#DDAAFF;\">%s</span>" ),
408 anchorName, escapedMatchStr );
409 }
410 else
411 {
412 out += wxString::Format( wxS( "<span style=\"background-color:#FFFFAA;\">%s</span>" ), escapedMatchStr );
413 }
414
415 start = idx + term.length();
416 }
417
418 out += m_originalSource.Mid( start );
419 m_source = out;
420 reload();
421
422 // Use CallAfter to ensure the HTML is fully loaded before scrolling
423 // Only scroll if we have matches and a valid current match
424 if( !m_matchPos.empty() && m_currentMatch >= 0 && m_currentMatch < (int)m_matchPos.size() )
425 {
426 CallAfter( [this]()
427 {
428 // Try to scroll to the anchor, with fallback if it fails
429 wxString anchorName = wxString::Format( wxS( "kicad_search_%d" ), m_currentMatch );
430 if( !m_htmlWindow->ScrollToAnchor( anchorName ) )
431 {
432 // If anchor scrolling fails, try to scroll to approximate position
433 // Calculate approximate scroll position based on match location
434 if( !m_matchPos.empty() && m_currentMatch < (int)m_matchPos.size() )
435 {
436 size_t matchPos = m_matchPos[m_currentMatch];
437 size_t totalLength = m_originalSource.length();
438 if( totalLength > 0 )
439 {
440 // Scroll to approximate percentage of document
441 double ratio = (double)matchPos / (double)totalLength;
442 int scrollPos = (int)(ratio * m_htmlWindow->GetScrollRange( wxVERTICAL ));
443 m_htmlWindow->Scroll( 0, scrollPos );
444 }
445 }
446 }
447 } );
448 }
449
450 m_matchCount->SetLabel( wxString::Format( wxS( "%d/%zu" ), m_currentMatch + 1, m_matchPos.size() ) );
451}
wxString GetMajorMinorVersion()
Get only the major and minor version in a string major.minor.
DIALOG_DISPLAY_HTML_TEXT_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=wxEmptyString, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
bool Show(bool show) override
void SetupStandardButtons(std::map< int, wxString > aLabels={})
void setSizeInDU(int x, int y)
Set the dialog to the given dimensions in "dialog units".
wxTextCtrl * m_searchCtrl
std::vector< size_t > m_matchPos
void OnSearchText(wxCommandEvent &aEvent)
void OnHTMLLinkClicked(wxHtmlLinkEvent &event) override
~HTML_MESSAGE_BOX() override
wxStaticText * m_matchCount
void OnPrev(wxCommandEvent &aEvent)
void MessageSet(const wxString &message)
Add a message (in bold) to message list.
void onThemeChanged(wxSysColourChangedEvent &aEvent)
virtual void OnCharHook(wxKeyEvent &aEvt) override
HTML_MESSAGE_BOX(wxWindow *aParent, const wxString &aTitle=wxEmptyString, const wxPoint &aPosition=wxDefaultPosition, const wxSize &aSize=wxDefaultSize)
void AddHTML_Text(const wxString &message)
Add HTML text (without any change) to message list.
void ListSet(const wxString &aList)
Add a list of items.
void OnNext(wxCommandEvent &aEvent)
void ShowModeless()
Show a modeless version of the dialog (without an OK button).
void wxStringSplit(const wxString &aText, wxArrayString &aStrings, wxChar aSplitter)
Split aString to a string list separated at aSplitter.