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