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