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/uri.h>
25#include <wx/utils.h>
26
27
28namespace
29{
30
31// Parse [label](url) starting at aStart
32bool tryParseMarkdown( const wxString& aText, size_t aStart, size_t& aEnd, wxString& aLabel, wxString& aHref )
33{
34 if( aStart >= aText.length() || aText[aStart] != '[' )
35 return false;
36
37 size_t labelEnd = aText.find( ']', aStart + 1 );
38
39 if( labelEnd == wxString::npos || labelEnd + 1 >= aText.length() || aText[labelEnd + 1] != '(' )
40 return false;
41
42 // Match balanced parens so URLs like "file:///foo (1).pdf" survive.
43 size_t hrefStart = labelEnd + 2;
44 size_t hrefEnd = wxString::npos;
45 int depth = 1;
46
47 for( size_t i = hrefStart; i < aText.length(); ++i )
48 {
49 wxChar c = aText[i];
50
51 if( c == '(' )
52 ++depth;
53 else if( c == ')' && --depth == 0 )
54 {
55 hrefEnd = i;
56 break;
57 }
58 }
59
60 if( hrefEnd == wxString::npos )
61 return false;
62
63 aLabel = aText.SubString( aStart + 1, labelEnd - 1 );
64 aHref = aText.SubString( hrefStart, hrefEnd - 1 );
65 aEnd = hrefEnd + 1;
66 return true;
67}
68
69
70void layoutRuns( wxDC& aDC, std::vector<HYPERLINK_DV_RENDERER::RUN>& aRuns, int aX, int aY, int aHeight )
71{
72 int x = aX;
73
74 for( HYPERLINK_DV_RENDERER::RUN& run : aRuns )
75 {
76 wxSize ext = aDC.GetTextExtent( run.text );
77 run.bounds = wxRect( x, aY, ext.x, aHeight > 0 ? aHeight : ext.y );
78 x += ext.x;
79 }
80}
81
82} // namespace
83
84
85bool HYPERLINK_DV_RENDERER::IsSafeUrl( const wxString& aHref )
86{
87 wxString lower = aHref.Lower();
88 wxURI uri( lower );
89
90 if( uri.GetScheme() == wxT( "http" ) || uri.GetScheme() == wxT( "https" ) )
91 return true;
92
93 if( uri.GetScheme() == wxT( "file" ) || lower.StartsWith( wxT( "\\\\" ) ) )
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 // The OS launches the decoded name, and Windows drops trailing dots and spaces.
101 wxString path = wxURI::Unescape( uri.GetPath() );
102
103 while( path.EndsWith( wxT( "." ) ) || path.EndsWith( wxT( " " ) ) )
104 path.RemoveLast();
105
106 for( const wxString& ext : blockedExtensions )
107 {
108 if( path.EndsWith( ext ) )
109 return false;
110 }
111
112 return true;
113 }
114
115 return false;
116}
117
118
119wxString HYPERLINK_DV_RENDERER::StripMarkup( const wxString& aValue )
120{
121 std::vector<RUN> runs;
122 ParseRuns( aValue, runs );
123
124 wxString result;
125
126 for( const RUN& run : runs )
127 result += run.text;
128
129 return result;
130}
131
132
133void HYPERLINK_DV_RENDERER::ParseRuns( const wxString& aValue, std::vector<RUN>& aRuns )
134{
135 aRuns.clear();
136
137 size_t pos = 0;
138 wxString plain;
139
140 while( pos < aValue.length() )
141 {
142 size_t end = pos;
143 wxString label;
144 wxString href;
145
146 if( aValue[pos] == '[' && tryParseMarkdown( aValue, pos, end, label, href ) )
147 {
148 if( !plain.IsEmpty() )
149 {
150 aRuns.push_back( { plain, wxEmptyString, wxRect() } );
151 plain.clear();
152 }
153
154 if( IsSafeUrl( href ) )
155 aRuns.push_back( { label, href, wxRect() } );
156 else
157 aRuns.push_back( { label, wxEmptyString, wxRect() } );
158
159 pos = end;
160 }
161 else
162 {
163 plain += aValue[pos];
164 ++pos;
165 }
166 }
167
168 if( !plain.IsEmpty() )
169 aRuns.push_back( { plain, wxEmptyString, wxRect() } );
170}
171
172
174 wxDataViewCustomRenderer( wxT( "string" ), wxDATAVIEW_CELL_INERT, wxDVR_DEFAULT_ALIGNMENT )
175{
176}
177
178
179bool HYPERLINK_DV_RENDERER::SetValue( const wxVariant& aValue )
180{
181 m_value = aValue.GetString();
183 return true;
184}
185
186
187bool HYPERLINK_DV_RENDERER::GetValue( wxVariant& aValue ) const
188{
189 aValue = m_value;
190 return true;
191}
192
193
195{
196 if( m_value.IsEmpty() )
197 return wxSize( wxDVC_DEFAULT_RENDERER_SIZE, wxDVC_DEFAULT_RENDERER_SIZE );
198
199 // Sum the visible-text width across runs (markup is hidden, so the raw
200 // m_value width over-reports).
201 wxSize size( 0, 0 );
202
203 for( const RUN& run : m_runs )
204 {
205 wxSize ext = GetTextExtent( run.text );
206 size.x += ext.x;
207 size.y = std::max( size.y, ext.y );
208 }
209
210 if( size.y == 0 )
211 size.y = wxDVC_DEFAULT_RENDERER_SIZE;
212
213 return size;
214}
215
216
217bool HYPERLINK_DV_RENDERER::Render( wxRect aCell, wxDC* aDC, int aState )
218{
219 RenderBackground( aDC, aCell );
220
221 layoutRuns( *aDC, m_runs, aCell.x, aCell.y, aCell.height );
222
223 const bool selected = ( aState & wxDATAVIEW_CELL_SELECTED ) != 0;
224 int xoffset = 0;
225
226#ifdef __WXOSX__
227 const int textState = 0;
228#else
229 const int textState = aState;
230#endif
231
232 for( const RUN& run : m_runs )
233 {
234 wxRect runRect = aCell;
235 runRect.x += xoffset;
236 runRect.width = run.bounds.width;
237
238 if( !run.href.IsEmpty() )
239 {
240 wxDCTextColourChanger col( *aDC, selected ? wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT )
241 : wxSystemSettings::GetColour( wxSYS_COLOUR_HOTLIGHT ) );
242
243 wxFont linkFont = aDC->GetFont();
244 linkFont.SetUnderlined( true );
245 wxDCFontChanger font( *aDC, linkFont );
246
247 RenderText( run.text, 0, runRect, aDC, textState );
248 }
249 else
250 {
251#ifdef __WXOSX__
252 if( selected )
253 aDC->SetTextForeground( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT ) );
254#endif
255 RenderText( run.text, 0, runRect, aDC, textState );
256 }
257
258 xoffset += run.bounds.width;
259 }
260
261 return true;
262}
263
264
265bool HYPERLINK_DV_RENDERER::HitTestRunsForCell( const wxString& aValue, const wxDataViewItemAttr& aCellAttr,
266 const wxRect& aCellRect, const wxPoint& aPoint, wxString* aHref ) const
267{
268 std::vector<RUN> runs;
269 ParseRuns( aValue, runs );
270
271 wxClientDC dc( const_cast<wxDataViewCtrl*>( GetView() ) );
272 wxFont font = GetView()->GetFont();
273
274 if( aCellAttr.GetBold() )
275 font.MakeBold();
276
277 if( aCellAttr.GetItalic() )
278 font.MakeItalic();
279
280 if( aCellAttr.GetStrikethrough() )
281 font.MakeStrikethrough();
282
283 dc.SetFont( font );
284 layoutRuns( dc, runs, aCellRect.x, aCellRect.y, aCellRect.height );
285
286 for( const RUN& run : runs )
287 {
288 if( !run.href.IsEmpty() && run.bounds.Contains( aPoint ) )
289 {
290 if( aHref )
291 *aHref = run.href;
292
293 return true;
294 }
295 }
296
297 return false;
298}
std::string path
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.