KiCad PCB EDA Suite
Loading...
Searching...
No Matches
notifications_manager.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) 2023 Mark Roszko <[email protected]>
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, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <wx/filename.h>
22#include <wx/frame.h>
23#include <wx/hyperlink.h>
24#include <wx/panel.h>
25#include <wx/scrolwin.h>
26#include <wx/sizer.h>
27#include <wx/settings.h>
28#include <wx/stattext.h>
29#include <wx/string.h>
30#include <wx/time.h>
31
32#include <paths.h>
33
35#include <widgets/kistatusbar.h>
36#include <widgets/ui_common.h>
37
38#include <algorithm>
39#include <json_common.h>
40#include <kiplatform/ui.h>
41
42#include <core/wx_stl_compat.h>
44#include <core/kicad_algo.h>
45
46#include <algorithm>
47#include <fstream>
48#include <map>
49#include <optional>
50#include <tuple>
51#include <vector>
52
53
54static long long g_last_closed_timer = 0;
55
56NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( NOTIFICATION, title, description, href, key, date )
57
58class NOTIFICATION_PANEL : public wxPanel
59{
60public:
61 NOTIFICATION_PANEL( wxWindow* aParent, NOTIFICATIONS_MANAGER* aManager, NOTIFICATION* aNoti ) :
62 wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxSize( -1, 75 ), wxBORDER_SIMPLE ),
63 m_hlDetails( nullptr ),
64 m_notification( aNoti ),
65 m_manager( aManager )
66 {
67 SetSizeHints( wxDefaultSize, wxDefaultSize );
68
69 wxBoxSizer* mainSizer;
70 mainSizer = new wxBoxSizer( wxVERTICAL );
71
72 wxColour fg, bg;
74 SetBackgroundColour( bg );
75 SetForegroundColour( fg );
76
77 m_stTitle = new wxStaticText( this, wxID_ANY, aNoti->title );
78 m_stTitle->Wrap( -1 );
79 m_stTitle->SetFont( KIUI::GetControlFont( this ).Bold() );
80 mainSizer->Add( m_stTitle, 0, wxALL | wxEXPAND, 1 );
81
82 m_stDescription = new wxStaticText( this, wxID_ANY, aNoti->description );
83 m_stDescription->Wrap( -1 );
84 mainSizer->Add( m_stDescription, 0, wxALL | wxEXPAND, 1 );
85
86 wxBoxSizer* tailSizer;
87 tailSizer = new wxBoxSizer( wxHORIZONTAL );
88
89 if( !aNoti->href.IsEmpty() )
90 {
91 m_hlDetails = new wxHyperlinkCtrl( this, wxID_ANY, _( "View Details" ), aNoti->href );
92 tailSizer->Add( m_hlDetails, 0, wxALL, 2 );
93 }
94
95 m_hlDismiss = new wxHyperlinkCtrl( this, wxID_ANY, _( "Dismiss" ), aNoti->href );
96 tailSizer->Add( m_hlDismiss, 0, wxALL, 2 );
97
98 mainSizer->Add( tailSizer, 1, wxEXPAND, 5 );
99
100 if( m_hlDetails != nullptr )
101 m_hlDetails->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDetails, this );
102
103 m_hlDismiss->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDismiss, this );
104
105 SetSizer( mainSizer );
106 Layout();
107 }
108
109private:
110 void onDetails( wxHyperlinkEvent& aEvent )
111 {
112 wxString url = aEvent.GetURL();
113
114 if( url.StartsWith( wxS( "kicad://" ) ) )
115 {
116 url.Replace( wxS( "kicad://" ), wxS( "" ) );
117
118 if( url == wxS( "pcm" ) )
119 {
120 // TODO
121 }
122 }
123 else
124 {
125 wxLaunchDefaultBrowser( aEvent.GetURL(), wxBROWSER_NEW_WINDOW );
126 }
127 }
128
129 void onDismiss( wxHyperlinkEvent& aEvent )
130 {
131 CallAfter(
132 [this]()
133 {
134 // This will cause this panel to get deleted
135 m_manager->Remove( m_notification->key );
136 } );
137 }
138
139private:
140 wxStaticText* m_stTitle;
141 wxStaticText* m_stDescription;
142 wxHyperlinkCtrl* m_hlDetails;
143 wxHyperlinkCtrl* m_hlDismiss;
146};
147
148
149class NOTIFICATIONS_LIST : public wxFrame
150{
151public:
152 NOTIFICATIONS_LIST( NOTIFICATIONS_MANAGER* aManager, wxWindow* parent, const wxPoint& pos ) :
153 wxFrame( parent, wxID_ANY, _( "Notifications" ), pos, wxSize( 300, 150 ),
154 wxFRAME_NO_TASKBAR | wxBORDER_SIMPLE ),
155 m_manager( aManager )
156 {
157 SetSizeHints( wxDefaultSize, wxDefaultSize );
158
159 wxBoxSizer* bSizer1;
160 bSizer1 = new wxBoxSizer( wxVERTICAL );
161
162 m_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition,
163 wxSize( -1, -1 ), wxVSCROLL | wxBORDER_SIMPLE );
164 wxColour fg, bg;
166 m_scrolledWindow->SetBackgroundColour( bg );
167 m_scrolledWindow->SetForegroundColour( fg );
168
169 m_scrolledWindow->SetScrollRate( 5, 5 );
170 m_contentSizer = new wxBoxSizer( wxVERTICAL );
171
172 m_scrolledWindow->SetSizer( m_contentSizer );
173 m_scrolledWindow->Layout();
175 bSizer1->Add( m_scrolledWindow, 1, wxEXPAND | wxALL, 0 );
176
177 m_noNotificationsText = new wxStaticText( m_scrolledWindow, wxID_ANY,
178 _( "There are no notifications available" ),
179 wxDefaultPosition, wxDefaultSize,
180 wxALIGN_CENTER_HORIZONTAL );
181 m_noNotificationsText->Wrap( -1 );
182 m_contentSizer->Add( m_noNotificationsText, 1, wxALL | wxEXPAND, 5 );
183
184 Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
185 m_scrolledWindow->Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
186
187 SetSizer( bSizer1 );
188 Layout();
189
190 SetFocus();
191 }
192
193
195 {
196 // Unbind focus handlers before base class destructors run. GTK fires focus-out signals
197 // during window teardown, which would invoke onFocusLoss through a partially-destroyed
198 // object and crash in the wxWidgets event handler chain.
199 Unbind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
200 m_scrolledWindow->Unbind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
201 }
202
203
204 void onFocusLoss( wxFocusEvent& aEvent )
205 {
206 if( IsDescendant( aEvent.GetWindow() ) )
207 {
208 // Child (such as the hyperlink texts) got focus
209 }
210 else
211 {
212 Close( true );
213 g_last_closed_timer = wxGetLocalTimeMillis().GetValue();
214 }
215
216 aEvent.Skip();
217 }
218
219
220 void Add( NOTIFICATION* aNoti )
221 {
222 m_noNotificationsText->Hide();
223
225 m_contentSizer->Add( panel, 0, wxEXPAND | wxALL, 2 );
226 m_scrolledWindow->Layout();
228
229 // call this at this window otherwise the child panels don't resize width properly
230 Layout();
231
232 m_panelMap[aNoti] = panel;
233 }
234
235
236 void Remove( NOTIFICATION* aNoti )
237 {
238 auto it = m_panelMap.find( aNoti );
239
240 if( it != m_panelMap.end() )
241 {
242 NOTIFICATION_PANEL* panel = m_panelMap[aNoti];
243 m_contentSizer->Detach( panel );
244 panel->Destroy();
245
246 m_panelMap.erase( it );
247
248 // ensure the window contents get shifted as needed
249 m_scrolledWindow->Layout();
250 Layout();
251 }
252
253 if( m_panelMap.size() == 0 )
254 {
255 m_noNotificationsText->Show();
256 }
257 }
258
259private:
260 wxScrolledWindow* m_scrolledWindow;
261
263 wxBoxSizer* m_contentSizer;
264 std::unordered_map<NOTIFICATION*, NOTIFICATION_PANEL*> m_panelMap;
266
270};
271
272
274{
275 m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) );
276}
277
278
280{
281 nlohmann::json saved_json;
282
283 std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() );
284
285 try
286 {
287 saved_json_stream >> saved_json;
288
289 m_notifications = saved_json.get<std::vector<NOTIFICATION>>();
290 }
291 catch( std::exception& )
292 {
293 // failed to load the json, which is fine, default to no notifications
294 }
295
296 if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) )
297 {
298 CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ),
299 wxS( "https://kicad.org" ) );
300 }
301}
302
303
305{
306 std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() );
307
308 nlohmann::json saveJson = nlohmann::json( m_notifications );
309 jsonFileStream << std::setw( 4 ) << saveJson << std::endl;
310 jsonFileStream.flush();
311 jsonFileStream.close();
312}
313
314
315void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey,
316 const wxString& aTitle,
317 const wxString& aDescription,
318 const wxString& aHref )
319{
320 wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) );
321
322 auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
323 [&]( const NOTIFICATION& noti )
324 {
325 return noti.key == aKey;
326 } );
327
328 if( it != m_notifications.end() )
329 {
330 NOTIFICATION& noti = *it;
331
332 noti.title = aTitle;
333 noti.description = aDescription;
334 noti.href = aHref;
335 }
336 else
337 {
338 m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref,
339 aKey, wxEmptyString } );
340 }
341
342 if( m_shownDialogs.size() > 0 )
343 {
344 // update dialogs
346 list->Add( &m_notifications.back() );
347 }
348
349 for( KISTATUSBAR* statusBar : m_statusBars )
350 statusBar->SetNotificationCount( m_notifications.size() );
351
352 Save();
353}
354
355
356void NOTIFICATIONS_MANAGER::Remove( const wxString& aKey )
357{
358 auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
359 [&]( const NOTIFICATION& noti )
360 {
361 return noti.key == aKey;
362 } );
363
364 if( it == m_notifications.end() )
365 return;
366
367 if( m_shownDialogs.size() > 0 )
368 {
369 // update dialogs
370
372 list->Remove( &(*it) );
373 }
374
375 m_notifications.erase( it );
376
377 Save();
378
379 for( KISTATUSBAR* statusBar : m_statusBars )
380 statusBar->SetNotificationCount( m_notifications.size() );
381}
382
383
385{
386 NOTIFICATIONS_LIST* evtWindow = dynamic_cast<NOTIFICATIONS_LIST*>( aEvent.GetEventObject() );
387
388 std::erase_if( m_shownDialogs, [&]( NOTIFICATIONS_LIST* dialog )
389 {
390 return dialog == evtWindow;
391 } );
392
393 aEvent.Skip();
394}
395
396
397void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos )
398{
399 // Debounce clicking on the icon with a list already showing. The button will get focus
400 // first, which will cause a focus-loss on the list (thereby closing it), and then we'd open
401 // it again without this guard.
402 if( wxGetLocalTimeMillis().GetValue() - g_last_closed_timer < 300 )
403 {
405 return;
406 }
407
408 NOTIFICATIONS_LIST* list = new NOTIFICATIONS_LIST( this, aParent, aPos );
409
410 for( NOTIFICATION& job : m_notifications )
411 list->Add( &job );
412
413 m_shownDialogs.push_back( list );
414
415 list->Bind( wxEVT_CLOSE_WINDOW, &NOTIFICATIONS_MANAGER::onListWindowClosed, this );
416
417 // correct the position
418 wxSize windowSize = list->GetSize();
419 list->SetPosition( aPos - windowSize );
420
421 list->Show();
423}
424
425
427{
428 m_statusBars.push_back( aStatusBar );
429
430 // notifications should already be loaded so set the initial notification count
431 aStatusBar->SetNotificationCount( m_notifications.size() );
432}
433
434
436{
437 std::erase_if( m_statusBars, [&]( KISTATUSBAR* statusBar )
438 {
439 return statusBar == aStatusBar;
440 } );
441}
void SetNotificationCount(int aCount)
Set the notification count on the notifications button.
void onFocusLoss(wxFocusEvent &aEvent)
void Add(NOTIFICATION *aNoti)
NOTIFICATIONS_MANAGER * m_manager
wxBoxSizer * m_contentSizer
Inner content of the scrolled window, add panels here.
std::unordered_map< NOTIFICATION *, NOTIFICATION_PANEL * > m_panelMap
void Remove(NOTIFICATION *aNoti)
wxScrolledWindow * m_scrolledWindow
wxStaticText * m_noNotificationsText
Text to be displayed when no notifications are present, this gets a Show/Hide call as needed.
NOTIFICATIONS_LIST(NOTIFICATIONS_MANAGER *aManager, wxWindow *parent, const wxPoint &pos)
void Save()
Save notifications to disk.
void onListWindowClosed(wxCloseEvent &aEvent)
Handle removing the shown list window from our list of shown windows.
wxFileName m_destFileName
The cached file path to read/write notifications on disk.
void Load()
Load notifications stored from disk.
void RegisterStatusBar(KISTATUSBAR *aStatusBar)
Add a status bar for handling.
void CreateOrUpdate(const wxString &aKey, const wxString &aTitle, const wxString &aDescription, const wxString &aHref=wxEmptyString)
Create a notification with the given parameters or updates an existing one with the same key.
void Remove(const wxString &aKey)
Remove a notification by key.
std::vector< KISTATUSBAR * > m_statusBars
Status bars registered for updates.
void UnregisterStatusBar(KISTATUSBAR *aStatusBar)
Remove status bar from handling.
std::vector< NOTIFICATION > m_notifications
Current stack of notifications.
void ShowList(wxWindow *aParent, wxPoint aPos)
Show the notification list.
std::vector< NOTIFICATIONS_LIST * > m_shownDialogs
Currently shown notification lists.
wxHyperlinkCtrl * m_hlDetails
void onDismiss(wxHyperlinkEvent &aEvent)
NOTIFICATION_PANEL(wxWindow *aParent, NOTIFICATIONS_MANAGER *aManager, NOTIFICATION *aNoti)
void onDetails(wxHyperlinkEvent &aEvent)
NOTIFICATIONS_MANAGER * m_manager
wxHyperlinkCtrl * m_hlDismiss
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition paths.cpp:460
#define _(s)
void GetInfoBarColours(wxColour &aFGColour, wxColour &aBGColour)
Return the background and foreground colors for info bars in the current scheme.
Definition wxgtk/ui.cpp:69
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition wxgtk/ui.cpp:126
KICOMMON_API wxFont GetControlFont(wxWindow *aWindow)
static long long g_last_closed_timer
wxString description
Additional message displayed under title.
wxString title
Title of the notification.
wxString href
URL if any to link to for details.
Functions to provide common constants and other functions to assist in making a consistent UI.
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UPDATE_REQUEST, platform, arch, current_version, lang, last_check) struct UPDATE_RESPONSE