KiCad PCB EDA Suite
Loading...
Searching...
No Matches
wx_data_view_hyperlink_renderer.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 The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <wx/dcclient.h>
23#include <wx/settings.h>
24#include <wx/utils.h>
25
26
27namespace
28{
29
30// Parse [label](url) starting at aStart
31bool tryParseMarkdown( const wxString& aText, size_t aStart, size_t& aEnd, wxString& aLabel, wxString& aHref )
32{
33 if( aStart >= aText.length() || aText[aStart] != '[' )
34 return false;
35
36 size_t labelEnd = aText.find( ']', aStart + 1 );
37
38 if( labelEnd == wxString::npos || labelEnd + 1 >= aText.length() || aText[labelEnd + 1] != '(' )
39 return false;
40
41 // Match balanced parens so URLs like "file:///foo (1).pdf" survive.
42 size_t hrefStart = labelEnd + 2;
43 size_t hrefEnd = wxString::npos;
44 int depth = 1;
45
46 for( size_t i = hrefStart; i < aText.length(); ++i )
47 {
48 wxChar c = aText[i];
49
50 if( c == '(' )
51 ++depth;
52 else if( c == ')' && --depth == 0 )
53 {
54 hrefEnd = i;
55 break;
56 }
57 }
58
59 if( hrefEnd == wxString::npos )
60 return false;
61
62 aLabel = aText.SubString( aStart + 1, labelEnd - 1 );
63 aHref = aText.SubString( hrefStart, hrefEnd - 1 );
64 aEnd = hrefEnd + 1;
65 return true;
66}
67
68
69void layoutRuns( wxDC& aDC, std::vector<HYPERLINK_DV_RENDERER::RUN>& aRuns, int aX, int aY, int aHeight )
70{
71 int x = aX;
72
73 for( HYPERLINK_DV_RENDERER::RUN& run : aRuns )
74 {
75 wxSize ext = aDC.GetTextExtent( run.text );
76 run.bounds = wxRect( x, aY, ext.x, aHeight > 0 ? aHeight : ext.y );
77 x += ext.x;
78 }
79}
80
81} // namespace
82
83
84bool HYPERLINK_DV_RENDERER::IsSafeUrl( const wxString& aHref )
85{
86 wxString lower = aHref.Lower();
87
88 if( lower.StartsWith( wxT( "http://" ) ) || lower.StartsWith( wxT( "https://" ) ) )
89 return true;
90
91 bool isLocalFile = lower.StartsWith( wxT( "file://" ) ) || lower.StartsWith( wxT( "\\\\" ) );
92
93 if( isLocalFile )
94 {
95 static const wxString blockedExtensions[] = { wxT( ".exe" ), wxT( ".com" ), wxT( ".bat" ), wxT( ".cmd" ),
96 wxT( ".ps1" ), wxT( ".vbs" ), wxT( ".js" ), wxT( ".jar" ),
97 wxT( ".msi" ), wxT( ".scr" ), wxT( ".pif" ), wxT( ".lnk" ),
98 wxT( ".hta" ) };
99
100 for( const wxString& ext : blockedExtensions )
101 {
102 if( lower.EndsWith( ext ) )
103 return false;
104 }
105
106 return true;
107 }
108
109 return false;
110}
111
112
113wxString HYPERLINK_DV_RENDERER::StripMarkup( const wxString& aValue )
114{
115 std::vector<RUN> runs;
116 ParseRuns( aValue, runs );
117
118 wxString result;
119
120 for( const RUN& run : runs )
121 result += run.text;
122
123 return result;
124}
125
126
127void HYPERLINK_DV_RENDERER::ParseRuns( const wxString& aValue, std::vector<RUN>& aRuns )
128{
129 aRuns.clear();
130
131 size_t pos = 0;
132 wxString plain;
133
134 while( pos < aValue.length() )
135 {
136 size_t end = pos;
137 wxString label;
138 wxString href;
139
140 if( aValue[pos] == '[' && tryParseMarkdown( aValue, pos, end, label, href ) )
141 {
142 if( !plain.IsEmpty() )
143 {
144 aRuns.push_back( { plain, wxEmptyString, wxRect() } );
145 plain.clear();
146 }
147
148 if( IsSafeUrl( href ) )
149 aRuns.push_back( { label, href, wxRect() } );
150 else
151 aRuns.push_back( { label, wxEmptyString, wxRect() } );
152
153 pos = end;
154 }
155 else
156 {
157 plain += aValue[pos];
158 ++pos;
159 }
160 }
161
162 if( !plain.IsEmpty() )
163 aRuns.push_back( { plain, wxEmptyString, wxRect() } );
164}
165
166
168 wxDataViewCustomRenderer( wxT( "string" ), wxDATAVIEW_CELL_INERT, wxDVR_DEFAULT_ALIGNMENT )
169{
170}
171
172
173bool HYPERLINK_DV_RENDERER::SetValue( const wxVariant& aValue )
174{
175 m_value = aValue.GetString();
177 return true;
178}
179
180
181bool HYPERLINK_DV_RENDERER::GetValue( wxVariant& aValue ) const
182{
183 aValue = m_value;
184 return true;
185}
186
187
189{
190 if( m_value.IsEmpty() )
191 return wxSize( wxDVC_DEFAULT_RENDERER_SIZE, wxDVC_DEFAULT_RENDERER_SIZE );
192
193 // Sum the visible-text width across runs (markup is hidden, so the raw
194 // m_value width over-reports).
195 wxSize size( 0, 0 );
196
197 for( const RUN& run : m_runs )
198 {
199 wxSize ext = GetTextExtent( run.text );
200 size.x += ext.x;
201 size.y = std::max( size.y, ext.y );
202 }
203
204 if( size.y == 0 )
205 size.y = wxDVC_DEFAULT_RENDERER_SIZE;
206
207 return size;
208}
209
210
211bool HYPERLINK_DV_RENDERER::Render( wxRect aCell, wxDC* aDC, int aState )
212{
213 RenderBackground( aDC, aCell );
214
215 // Capture aDC's font so HitTestRunsForCell can measure with the same font.
216 m_renderFont = aDC->GetFont();
217
218 layoutRuns( *aDC, m_runs, aCell.x, aCell.y, aCell.height );
219
220 const bool selected = ( aState & wxDATAVIEW_CELL_SELECTED ) != 0;
221 int xoffset = 0;
222
223#ifdef __WXOSX__
224 const int textState = 0;
225#else
226 const int textState = aState;
227#endif
228
229 for( const RUN& run : m_runs )
230 {
231 wxRect runRect = aCell;
232 runRect.x += xoffset;
233 runRect.width = run.bounds.width;
234
235 if( !run.href.IsEmpty() )
236 {
237 wxDCTextColourChanger col( *aDC, selected ? wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT )
238 : wxSystemSettings::GetColour( wxSYS_COLOUR_HOTLIGHT ) );
239
240 wxFont linkFont = aDC->GetFont();
241 linkFont.SetUnderlined( true );
242 wxDCFontChanger font( *aDC, linkFont );
243
244 RenderText( run.text, 0, runRect, aDC, textState );
245 }
246 else
247 {
248#ifdef __WXOSX__
249 if( selected )
250 aDC->SetTextForeground( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT ) );
251#endif
252 RenderText( run.text, 0, runRect, aDC, textState );
253 }
254
255 xoffset += run.bounds.width;
256 }
257
258 return true;
259}
260
261
262bool HYPERLINK_DV_RENDERER::HitTestRunsForCell( const wxString& aValue, const wxRect& aCell, const wxPoint& aPoint,
263 wxString* aHref ) const
264{
265 std::vector<RUN> runs;
266 ParseRuns( aValue, runs );
267
268 wxClientDC dc( const_cast<wxDataViewCtrl*>( GetView() ) );
269 dc.SetFont( m_renderFont.IsOk() ? m_renderFont : GetView()->GetFont() );
270 layoutRuns( dc, runs, aCell.x, aCell.y, aCell.height );
271
272 for( const RUN& run : runs )
273 {
274 if( !run.href.IsEmpty() && run.bounds.Contains( aPoint ) )
275 {
276 if( aHref )
277 *aHref = run.href;
278
279 return true;
280 }
281 }
282
283 return false;
284}
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.