KiCad PCB EDA Suite
Loading...
Searching...
No Matches
git_file_diff_area.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21
22#include <algorithm>
23#include <fstream>
24#include <optional>
25#include <utility>
26
27#include <wx/sizer.h>
28#include <wx/splitter.h>
29#include <wx/stattext.h>
30#include <wx/stc/stc.h>
31#include <wx/utils.h>
32
36#include <eda_draw_frame.h>
37#include <git/text_diff.h>
38#include <kiway.h>
39#include <project.h>
40#include <reporter.h>
41#include <view/view.h>
43
44
45namespace
46{
47
48// Where to open the before and after canvases: on the first change, padded to a readable size.
49std::optional<BOX2I> firstChangeBox( const KICAD_DIFF::DIFF_SCENE& aScene, KICAD_DIFF::DOC_KIND aKind )
50{
51 for( const std::vector<KICAD_DIFF::SCENE_SHAPE>* shapes :
52 { &aScene.modifiedShapes, &aScene.addedShapes, &aScene.removedShapes } )
53 {
54 if( shapes->empty() )
55 continue;
56
57 BOX2I box = shapes->front().bbox;
58
59 if( box.GetWidth() <= 0 || box.GetHeight() <= 0 )
60 continue;
61
62 int minSide = KICAD_DIFF::IuScaleForDocKind( aKind ).mmToIU( 50 );
63 int padX = static_cast<int>( minSide - box.GetWidth() ) / 2;
64 int padY = static_cast<int>( minSide - box.GetHeight() ) / 2;
65 box.Inflate( std::max( 0, padX ), std::max( 0, padY ) );
66 return box;
67 }
68
69 return std::nullopt;
70}
71
72
73std::string readAll( const wxString& aFile )
74{
75 if( aFile.IsEmpty() )
76 return std::string();
77
78 std::ifstream in( aFile.fn_str(), std::ios::binary );
79 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
80}
81
82
83struct FILE_DIFF_RESULT
84{
85 // Keep the extracted files until the UI has loaded the drawing documents.
86 std::unique_ptr<GIT_DIFF_WORKSPACE> workspace;
87 GIT_FILE_SIDES sides;
88 std::vector<KIGIT::TEXT_DIFF_LINE> textLines;
89 bool ready = false;
90};
91
92} // namespace
93
94
96 wxPanel( aParent ),
97 m_frame( aFrame ),
98 m_note( nullptr ),
99 m_canvasSplitter( nullptr ),
100 m_beforeLabel( nullptr ),
101 m_afterLabel( nullptr ),
102 m_beforeCanvas( nullptr ),
103 m_afterCanvas( nullptr ),
104 m_textDiff( nullptr ),
105 m_legend( nullptr )
106{
107 wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
108
109 // One of these shows at a time: a note, the two drawings, or the line diff.
110 m_note = new wxStaticText( this, wxID_ANY, wxEmptyString );
111 m_canvasSplitter = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
112 wxSP_LIVE_UPDATE | wxSP_3DSASH );
113 m_canvasSplitter->SetMinimumPaneSize( FromDIP( 60 ) );
114 m_canvasSplitter->SetSashGravity( 0.5 );
115 m_textDiff = new wxStyledTextCtrl( this, wxID_ANY );
116
117 // What the marks on the drawings mean, in the colours they are drawn with.
119 m_legend = new wxBoxSizer( wxHORIZONTAL );
120
121 for( const auto& [colour, label] : { std::make_pair( theme.added, _( "Added" ) ),
122 std::make_pair( theme.removed, _( "Removed" ) ),
123 std::make_pair( theme.modified, _( "Modified" ) ) } )
124 {
125 wxPanel* swatch = new wxPanel( this, wxID_ANY, wxDefaultPosition, FromDIP( wxSize( 12, 12 ) ) );
126 swatch->SetBackgroundColour( colour.ToColour() );
127 m_legend->Add( swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4 );
128 m_legend->Add( new wxStaticText( this, wxID_ANY, label ), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 16 );
129 }
130
131 sizer->Add( m_note, 0, wxEXPAND | wxALL, 4 );
132 sizer->Add( m_canvasSplitter, 1, wxEXPAND );
133 sizer->Add( m_legend, 0, wxLEFT | wxTOP, 4 );
134 sizer->Add( m_textDiff, 1, wxEXPAND );
135 SetSizer( sizer );
136
137 m_note->Hide();
138 m_canvasSplitter->Hide();
139 sizer->Show( m_legend, false );
140 m_textDiff->Hide();
141}
142
143
145{
146 m_query.Cancel();
147
148 // The canvases still point at the documents, so the documents go first.
149 m_diffContext.reset();
150}
151
152
154{
155 m_query.Cancel();
157}
158
159
160void GIT_FILE_DIFF_AREA::ShowFile( const std::shared_ptr<KIGIT_COMMON>& aSession, const MATERIALIZE& aMaterialize,
161 const wxString& aBeforeLabel, const wxString& aAfterLabel )
162{
163 showNote( _( "Loading comparison..." ) );
164
165 auto result = std::make_shared<FILE_DIFF_RESULT>();
166 result->sides.error = _( "Could not compare this file." );
167
168 m_query.Run(
169 aSession,
170 [result, aMaterialize]( KIGIT_COMMON& aCommon )
171 {
172 result->workspace = std::make_unique<GIT_DIFF_WORKSPACE>();
173
174 if( !result->workspace->IsOk() )
175 {
176 result->sides.error = _( "Could not create a temporary directory for the diff." );
177 return;
178 }
179
180 if( !aMaterialize( aCommon, *result->workspace, result->sides ) )
181 return;
182
183 if( result->sides.kind == KICAD_DIFF::DOC_KIND::UNKNOWN )
184 {
185 std::string before = readAll( result->sides.before );
186 std::string after = readAll( result->sides.after );
187
188 if( KIGIT::TextLooksBinary( before ) || KIGIT::TextLooksBinary( after ) )
189 {
190 result->sides.error = _( "This is not a plain text file, so there is no comparison to show." );
191 return;
192 }
193
194 result->textLines =
196 }
197
198 result->ready = true;
199 },
200 [this, result, aSession, aBeforeLabel, aAfterLabel]()
201 {
202 if( m_frame->Prj().GetGitCommon() != aSession )
203 return;
204
205 if( !result->ready )
206 {
207 showNote( result->sides.error );
208 return;
209 }
210
211 // Kiface loading and canvas creation must stay on the UI thread.
212 showDiff( result->sides, result->textLines, aBeforeLabel, aAfterLabel );
213 } );
214}
215
216
217void GIT_FILE_DIFF_AREA::showNote( const wxString& aNote )
218{
220 m_note->SetLabel( aNote );
221 m_note->Show();
222 m_canvasSplitter->Hide();
223 GetSizer()->Show( m_legend, false );
224 m_textDiff->Hide();
225 Layout();
226}
227
228
229void GIT_FILE_DIFF_AREA::showDiff( const GIT_FILE_SIDES& aSides, const std::vector<KIGIT::TEXT_DIFF_LINE>& aTextLines,
230 const wxString& aBeforeLabel, const wxString& aAfterLabel )
231{
232 bool shown = true;
233
234 if( aSides.kind == KICAD_DIFF::DOC_KIND::UNKNOWN )
235 showTextDiff( aTextLines );
236 else if( aSides.kind == KICAD_DIFF::DOC_KIND::SCH || aSides.kind == KICAD_DIFF::DOC_KIND::PCB )
237 shown = showDrawingDiff( aSides.kind, aSides.before, aSides.after, aBeforeLabel, aAfterLabel );
238 else
239 showNote( _( "Double click the file to open its comparison." ) );
240
241 if( shown )
242 Layout();
243}
244
245
246void GIT_FILE_DIFF_AREA::showTextDiff( const std::vector<KIGIT::TEXT_DIFF_LINE>& aLines )
247{
250 m_note->Hide();
251 m_canvasSplitter->Hide();
252 GetSizer()->Show( m_legend, false );
253 m_textDiff->Show();
254}
255
256
258 const wxString& aAfter, const wxString& aBeforeLabel,
259 const wxString& aAfterLabel )
260{
262
263 // The old documents come off the canvases before the new ones go on.
264 m_diffContext.reset();
265
268
269 m_diffContext = KICAD_DIFF::DispatchAttachDiffCanvases( m_frame->Kiway(), aKind, aBefore, aAfter,
271
272 if( !m_diffContext )
273 {
274 showNote( reporter.HasMessage() ? reporter.GetMessages() : _( "Could not compare this file." ) );
275 return false;
276 }
277
279 KICAD_DIFF::DIFF_SCENE scene = KICAD_DIFF::BuildScene( diff, theme );
280 scene.docKind = aKind;
281
282 // Both sides open on the same change. The user pans from there and the other side follows.
283 std::optional<BOX2I> focus = firstChangeBox( scene, aKind );
284 m_beforeCanvas->SetFocusBox( focus );
285 m_afterCanvas->SetFocusBox( focus );
286
288
289 m_beforeLabel->SetLabel( aBeforeLabel );
290 m_afterLabel->SetLabel( aAfterLabel );
291 m_beforeCanvas->SetScene( std::move( sides.reference ) );
292 m_afterCanvas->SetScene( std::move( sides.comparison ) );
293
294 m_note->Hide();
295 m_textDiff->Hide();
296 m_canvasSplitter->Show();
297 GetSizer()->Show( m_legend, true );
298 return true;
299}
300
301
303{
304 if( m_beforeCanvas )
305 return;
306
307 // The editor's own backend where there is one. The project manager has no canvas.
309
310 if( EDA_DRAW_FRAME* frame = dynamic_cast<EDA_DRAW_FRAME*>( m_frame ) )
311 {
312 if( frame->GetCanvas() )
313 backend = frame->GetCanvas()->GetBackend();
314 }
315
316 auto makeSide = [&]( wxStaticText** aLabel, WIDGET_DIFF_CANVAS** aCanvas )
317 {
318 wxPanel* side = new wxPanel( m_canvasSplitter );
319 wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
320
321 *aLabel = new wxStaticText( side, wxID_ANY, wxEmptyString );
322 *aCanvas = new WIDGET_DIFF_CANVAS( side, backend );
323
324 sizer->Add( *aLabel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 4 );
325 sizer->Add( *aCanvas, 1, wxEXPAND );
326 side->SetSizer( sizer );
327 return side;
328 };
329
330 wxPanel* before = makeSide( &m_beforeLabel, &m_beforeCanvas );
331 wxPanel* after = makeSide( &m_afterLabel, &m_afterCanvas );
332
333 m_canvasSplitter->SplitVertically( before, after );
334
335 // There is nothing to select here, so the left button pans like the middle one.
336 m_beforeCanvas->SetLeftDragPans( true );
337 m_afterCanvas->SetLeftDragPans( true );
338
341}
342
343
345{
346 // The documents come off the canvases before the canvases go.
347 m_diffContext.reset();
348
349 if( !m_beforeCanvas )
350 return;
351
352 // The splitter keeps its pane pointers through DestroyChildren, and its best size reads
353 // them, so take the panes out of it first.
354 m_canvasSplitter->Unsplit();
355 m_canvasSplitter->Initialize( nullptr );
356 m_canvasSplitter->DestroyChildren();
357 m_beforeCanvas = nullptr;
358 m_afterCanvas = nullptr;
359 m_beforeLabel = nullptr;
360 m_afterLabel = nullptr;
361}
362
363
365{
366 // The view controls handle the event after this, so the copy waits until they have.
367 auto onInput = [this, aLead, aFollower]( wxMouseEvent& aEvent )
368 {
369 aEvent.Skip();
370
371 CallAfter(
372 [this, aLead, aFollower]()
373 {
374 // The canvases may have gone with a file change before this ran.
375 if( aLead != m_beforeCanvas && aLead != m_afterCanvas )
376 return;
377
378 KIGFX::VIEW* lead = aLead->GetView();
379 KIGFX::VIEW* follower = aFollower->GetView();
380
381 if( follower->GetScale() == lead->GetScale() && follower->GetCenter() == lead->GetCenter() )
382 return;
383
384 follower->SetScale( lead->GetScale() );
385 follower->SetCenter( lead->GetCenter() );
386 aFollower->Refresh();
387 } );
388 };
389
390 aLead->Bind( wxEVT_MOTION, onInput );
391 aLead->Bind( wxEVT_MOUSEWHEEL, onInput );
392 aLead->Bind( wxEVT_MAGNIFY, onInput );
393 aLead->Bind( wxEVT_LEFT_UP, onInput );
394 aLead->Bind( wxEVT_MIDDLE_UP, onInput );
395}
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:553
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr size_type GetHeight() const
Definition box2.h:212
The base frame for deriving all KiCad main window classes.
The base class for create windows for drawing purpose.
static constexpr GAL_TYPE GAL_FALLBACK
virtual KIGFX::VIEW * GetView() const
Return a pointer to the KIGFX::VIEW instance used in the panel.
virtual void Refresh(bool aEraseBackground=true, const wxRect *aRect=nullptr) override
std::function< bool(KIGIT_COMMON &, GIT_DIFF_WORKSPACE &, GIT_FILE_SIDES &)> MATERIALIZE
Writes the two sides on a worker with the session's action lock held.
wxStyledTextCtrl * m_textDiff
KIGIT_QUERY m_query
A newer file selection discards any materialization still running for the previous one.
void showDiff(const GIT_FILE_SIDES &aSides, const std::vector< KIGIT::TEXT_DIFF_LINE > &aTextLines, const wxString &aBeforeLabel, const wxString &aAfterLabel)
GIT_FILE_DIFF_AREA(wxWindow *aParent, EDA_BASE_FRAME *aFrame)
void followCanvas(WIDGET_DIFF_CANVAS *aLead, WIDGET_DIFF_CANVAS *aFollower)
Give aFollower the view of aLead, so panning or zooming one moves both.
bool showDrawingDiff(KICAD_DIFF::DOC_KIND aKind, const wxString &aBefore, const wxString &aAfter, const wxString &aBeforeLabel, const wxString &aAfterLabel)
void showTextDiff(const std::vector< KIGIT::TEXT_DIFF_LINE > &aLines)
std::unique_ptr< KICAD_DIFF::DIFF_CANVAS_CONTEXT > m_diffContext
The documents the two canvases draw, held by the kiface that loaded them.
WIDGET_DIFF_CANVAS * m_afterCanvas
wxStaticText * m_beforeLabel
wxSplitterWindow * m_canvasSplitter
void ShowFile(const std::shared_ptr< KIGIT_COMMON > &aSession, const MATERIALIZE &aMaterialize, const wxString &aBeforeLabel, const wxString &aAfterLabel)
Compare the two sides aMaterialize writes out, or show its reason instead.
EDA_BASE_FRAME * m_frame
void showNote(const wxString &aNote)
void ensureCanvases()
The two drawing canvases exist only while a drawing is compared.
WIDGET_DIFF_CANVAS * m_beforeCanvas
wxStaticText * m_afterLabel
void Clear()
Drop the documents and the drawing canvases.
Hold a (potentially large) number of VIEW_ITEMs and renders them on a graphics device provided by the...
Definition view.h:68
double GetScale() const
Definition view.h:298
const VECTOR2D & GetCenter() const
Return the center point of this VIEW (in world space coordinates).
Definition view.h:377
virtual void SetScale(double aScale, VECTOR2D aAnchor={ 0, 0 })
Set the scaling factor, zooming around a given anchor point.
Definition view.cpp:901
void SetCenter(const VECTOR2D &aCenter)
Set the center point of the VIEW (i.e.
Definition view.cpp:971
The project's git session.
GAL-backed canvas for visualizing a KICAD_DIFF::DIFF_SCENE.
A wrapper for reporting to a wxString object.
Definition reporter.h:242
void FillTextDiffControl(wxStyledTextCtrl *aCtrl, const std::vector< KIGIT::TEXT_DIFF_LINE > &aLines)
Fill aCtrl with a line diff, added and removed lines in the diff colours, and make it read only.
#define _(s)
DIFF_SCENE BuildScene(const DOCUMENT_DIFF &aDiff, const DIFF_COLOR_THEME &aTheme)
Build a DIFF_SCENE from a DOCUMENT_DIFF, populating the shape lists and computing the union bbox.
SIDE_BY_SIDE_SCENES SplitSceneBySide(const DIFF_SCENE &aScene)
const EDA_IU_SCALE & IuScaleForDocKind(DOC_KIND aKind)
Internal-unit scale a document kind's coordinates are expressed in.
Definition diff_scene.h:253
std::unique_ptr< DIFF_CANVAS_CONTEXT > DispatchAttachDiffCanvases(KIWAY &aKiway, DOC_KIND aKind, const wxString &aFileA, const wxString &aFileB, WIDGET_DIFF_CANVAS &aBefore, WIDGET_DIFF_CANVAS &aAfter, DOCUMENT_DIFF &aDiff, REPORTER *aReporter)
Diff two files in the owning kiface through its KIFACE_ATTACH_DIFF_CANVASES export.
DOC_KIND
Document type a diff/merge entry point should route to, derived from a file path's extension.
std::vector< wxString > SplitTextLines(const std::string &aContent)
Tolerates CRLF endings and content that is not valid UTF-8.
Definition text_diff.cpp:28
bool TextLooksBinary(const std::string &aContent)
Definition text_diff.cpp:68
std::vector< TEXT_DIFF_LINE > BuildTextDiff(const std::vector< wxString > &aOld, const std::vector< wxString > &aNew)
Every line of both sides appears once. There are no hunks and nothing is elided.
Definition text_diff.cpp:77
constexpr int mmToIU(double mm) const
Definition base_units.h:92
The two sides of one file, written out for a comparison.
KICAD_DIFF::DOC_KIND kind
wxString before
Empty when the file did not exist on that side.
Diff colors with defaults for light backgrounds.
Definition diff_scene.h:275
std::vector< SCENE_SHAPE > modifiedShapes
Definition diff_scene.h:232
std::vector< SCENE_SHAPE > addedShapes
Definition diff_scene.h:230
DOC_KIND docKind
Source document type.
Definition diff_scene.h:227
std::vector< SCENE_SHAPE > removedShapes
Definition diff_scene.h:231
The full set of changes between two parsed documents of one type.
The two halves of a side by side view.
Definition diff_scene.h:330
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.