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, 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/filename.h>
26#include <wx/frame.h>
27#include <wx/hyperlink.h>
28#include <wx/panel.h>
29#include <wx/scrolwin.h>
30#include <wx/sizer.h>
31#include <wx/settings.h>
32#include <wx/stattext.h>
33#include <wx/string.h>
34#include <wx/time.h>
35
36#include <paths.h>
37
39#include <widgets/kistatusbar.h>
40#include <widgets/ui_common.h>
41
42#include <algorithm>
43#include <json_common.h>
44#include <kiplatform/ui.h>
45
46#include <core/wx_stl_compat.h>
48#include <core/kicad_algo.h>
49
50#include <algorithm>
51#include <fstream>
52#include <map>
53#include <optional>
54#include <tuple>
55#include <vector>
56
57
58static long long g_last_closed_timer = 0;
59
60NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( NOTIFICATION, title, description, href, key, date )
61
62class NOTIFICATION_PANEL : public wxPanel
63{
64public:
65 NOTIFICATION_PANEL( wxWindow* aParent, NOTIFICATIONS_MANAGER* aManager, NOTIFICATION* aNoti ) :
66 wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxSize( -1, 75 ), wxBORDER_SIMPLE ),
67 m_hlDetails( nullptr ),
68 m_notification( aNoti ),
69 m_manager( aManager )
70 {
71 SetSizeHints( wxDefaultSize, wxDefaultSize );
72
73 wxBoxSizer* mainSizer;
74 mainSizer = new wxBoxSizer( wxVERTICAL );
75
76 wxColour fg, bg;
78 SetBackgroundColour( bg );
79 SetForegroundColour( fg );
80
81 m_stTitle = new wxStaticText( this, wxID_ANY, aNoti->title );
82 m_stTitle->Wrap( -1 );
83 m_stTitle->SetFont( KIUI::GetControlFont( this ).Bold() );
84 mainSizer->Add( m_stTitle, 0, wxALL | wxEXPAND, 1 );
85
86 m_stDescription = new wxStaticText( this, wxID_ANY, aNoti->description );
87 m_stDescription->Wrap( -1 );
88 mainSizer->Add( m_stDescription, 0, wxALL | wxEXPAND, 1 );
89
90 wxBoxSizer* tailSizer;
91 tailSizer = new wxBoxSizer( wxHORIZONTAL );
92
93 if( !aNoti->href.IsEmpty() )
94 {
95 m_hlDetails = new wxHyperlinkCtrl( this, wxID_ANY, _( "View Details" ), aNoti->href );
96 tailSizer->Add( m_hlDetails, 0, wxALL, 2 );
97 }
98
99 m_hlDismiss = new wxHyperlinkCtrl( this, wxID_ANY, _( "Dismiss" ), aNoti->href );
100 tailSizer->Add( m_hlDismiss, 0, wxALL, 2 );
101
102 mainSizer->Add( tailSizer, 1, wxEXPAND, 5 );
103
104 if( m_hlDetails != nullptr )
105 m_hlDetails->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDetails, this );
106
107 m_hlDismiss->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDismiss, this );
108
109 SetSizer( mainSizer );
110 Layout();
111 }
112
113private:
114 void onDetails( wxHyperlinkEvent& aEvent )
115 {
116 wxString url = aEvent.GetURL();
117
118 if( url.StartsWith( wxS( "kicad://" ) ) )
119 {
120 url.Replace( wxS( "kicad://" ), wxS( "" ) );
121
122 if( url == wxS( "pcm" ) )
123 {
124 // TODO
125 }
126 }
127 else
128 {
129 wxLaunchDefaultBrowser( aEvent.GetURL(), wxBROWSER_NEW_WINDOW );
130 }
131 }
132
133 void onDismiss( wxHyperlinkEvent& aEvent )
134 {
135 CallAfter(
136 [this]()
137 {
138 // This will cause this panel to get deleted
139 m_manager->Remove( m_notification->key );
140 } );
141 }
142
143private:
144 wxStaticText* m_stTitle;
145 wxStaticText* m_stDescription;
146 wxHyperlinkCtrl* m_hlDetails;
147 wxHyperlinkCtrl* m_hlDismiss;
150};
151
152
153class NOTIFICATIONS_LIST : public wxFrame
154{
155public:
156 NOTIFICATIONS_LIST( NOTIFICATIONS_MANAGER* aManager, wxWindow* parent, const wxPoint& pos ) :
157 wxFrame( parent, wxID_ANY, _( "Notifications" ), pos, wxSize( 300, 150 ),
158 wxFRAME_NO_TASKBAR | wxBORDER_SIMPLE ),
159 m_manager( aManager )
160 {
161 SetSizeHints( wxDefaultSize, wxDefaultSize );
162
163 wxBoxSizer* bSizer1;
164 bSizer1 = new wxBoxSizer( wxVERTICAL );
165
166 m_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition,
167 wxSize( -1, -1 ), wxVSCROLL | wxBORDER_SIMPLE );
168 wxColour fg, bg;
170 m_scrolledWindow->SetBackgroundColour( bg );
171 m_scrolledWindow->SetForegroundColour( fg );
172
173 m_scrolledWindow->SetScrollRate( 5, 5 );
174 m_contentSizer = new wxBoxSizer( wxVERTICAL );
175
176 m_scrolledWindow->SetSizer( m_contentSizer );
177 m_scrolledWindow->Layout();
179 bSizer1->Add( m_scrolledWindow, 1, wxEXPAND | wxALL, 0 );
180
181 m_noNotificationsText = new wxStaticText( m_scrolledWindow, wxID_ANY,
182 _( "There are no notifications available" ),
183 wxDefaultPosition, wxDefaultSize,
184 wxALIGN_CENTER_HORIZONTAL );
185 m_noNotificationsText->Wrap( -1 );
186 m_contentSizer->Add( m_noNotificationsText, 1, wxALL | wxEXPAND, 5 );
187
188 Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
189 m_scrolledWindow->Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
190
191 SetSizer( bSizer1 );
192 Layout();
193
194 SetFocus();
195 }
196
197
198 void onFocusLoss( wxFocusEvent& aEvent )
199 {
200 if( IsDescendant( aEvent.GetWindow() ) )
201 {
202 // Child (such as the hyperlink texts) got focus
203 }
204 else
205 {
206 Close( true );
207 g_last_closed_timer = wxGetLocalTimeMillis().GetValue();
208 }
209
210 aEvent.Skip();
211 }
212
213
214 void Add( NOTIFICATION* aNoti )
215 {
216 m_noNotificationsText->Hide();
217
219 m_contentSizer->Add( panel, 0, wxEXPAND | wxALL, 2 );
220 m_scrolledWindow->Layout();
222
223 // call this at this window otherwise the child panels don't resize width properly
224 Layout();
225
226 m_panelMap[aNoti] = panel;
227 }
228
229
230 void Remove( NOTIFICATION* aNoti )
231 {
232 auto it = m_panelMap.find( aNoti );
233
234 if( it != m_panelMap.end() )
235 {
236 NOTIFICATION_PANEL* panel = m_panelMap[aNoti];
237 m_contentSizer->Detach( panel );
238 panel->Destroy();
239
240 m_panelMap.erase( it );
241
242 // ensure the window contents get shifted as needed
243 m_scrolledWindow->Layout();
244 Layout();
245 }
246
247 if( m_panelMap.size() == 0 )
248 {
249 m_noNotificationsText->Show();
250 }
251 }
252
253private:
254 wxScrolledWindow* m_scrolledWindow;
255
257 wxBoxSizer* m_contentSizer;
258 std::unordered_map<NOTIFICATION*, NOTIFICATION_PANEL*> m_panelMap;
260
264};
265
266
268{
269 m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) );
270}
271
272
274{
275 nlohmann::json saved_json;
276
277 std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() );
278
279 try
280 {
281 saved_json_stream >> saved_json;
282
283 m_notifications = saved_json.get<std::vector<NOTIFICATION>>();
284 }
285 catch( std::exception& )
286 {
287 // failed to load the json, which is fine, default to no notifications
288 }
289
290 if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) )
291 {
292 CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ),
293 wxS( "https://kicad.org" ) );
294 }
295}
296
297
299{
300 std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() );
301
302 nlohmann::json saveJson = nlohmann::json( m_notifications );
303 jsonFileStream << std::setw( 4 ) << saveJson << std::endl;
304 jsonFileStream.flush();
305 jsonFileStream.close();
306}
307
308
309void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey,
310 const wxString& aTitle,
311 const wxString& aDescription,
312 const wxString& aHref )
313{
314 wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) );
315
316 auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
317 [&]( const NOTIFICATION& noti )
318 {
319 return noti.key == aKey;
320 } );
321
322 if( it != m_notifications.end() )
323 {
324 NOTIFICATION& noti = *it;
325
326 noti.title = aTitle;
327 noti.description = aDescription;
328 noti.href = aHref;
329 }
330 else
331 {
332 m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref,
333 aKey, wxEmptyString } );
334 }
335
336 if( m_shownDialogs.size() > 0 )
337 {
338 // update dialogs
340 list->Add( &m_notifications.back() );
341 }
342
343 for( KISTATUSBAR* statusBar : m_statusBars )
344 statusBar->SetNotificationCount( m_notifications.size() );
345
346 Save();
347}
348
349
350void NOTIFICATIONS_MANAGER::Remove( const wxString& aKey )
351{
352 auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
353 [&]( const NOTIFICATION& noti )
354 {
355 return noti.key == aKey;
356 } );
357
358 if( it == m_notifications.end() )
359 return;
360
361 if( m_shownDialogs.size() > 0 )
362 {
363 // update dialogs
364
366 list->Remove( &(*it) );
367 }
368
369 m_notifications.erase( it );
370
371 Save();
372
373 for( KISTATUSBAR* statusBar : m_statusBars )
374 statusBar->SetNotificationCount( m_notifications.size() );
375}
376
377
379{
380 NOTIFICATIONS_LIST* evtWindow = dynamic_cast<NOTIFICATIONS_LIST*>( aEvent.GetEventObject() );
381
382 std::erase_if( m_shownDialogs, [&]( NOTIFICATIONS_LIST* dialog )
383 {
384 return dialog == evtWindow;
385 } );
386
387 aEvent.Skip();
388}
389
390
391void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos )
392{
393 // Debounce clicking on the icon with a list already showing. The button will get focus
394 // first, which will cause a focus-loss on the list (thereby closing it), and then we'd open
395 // it again without this guard.
396 if( wxGetLocalTimeMillis().GetValue() - g_last_closed_timer < 300 )
397 {
399 return;
400 }
401
402 NOTIFICATIONS_LIST* list = new NOTIFICATIONS_LIST( this, aParent, aPos );
403
404 for( NOTIFICATION& job : m_notifications )
405 list->Add( &job );
406
407 m_shownDialogs.push_back( list );
408
409 list->Bind( wxEVT_CLOSE_WINDOW, &NOTIFICATIONS_MANAGER::onListWindowClosed, this );
410
411 // correct the position
412 wxSize windowSize = list->GetSize();
413 list->SetPosition( aPos - windowSize );
414
415 list->Show();
417}
418
419
421{
422 m_statusBars.push_back( aStatusBar );
423
424 // notifications should already be loaded so set the initial notification count
425 aStatusBar->SetNotificationCount( m_notifications.size() );
426}
427
428
430{
431 std::erase_if( m_statusBars, [&]( KISTATUSBAR* statusBar )
432 {
433 return statusBar == aStatusBar;
434 } );
435}
KISTATUSBAR is a wxStatusBar suitable for Kicad manager.
Definition: kistatusbar.h:45
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:419
#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:67
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition: wxgtk/ui.cpp:124
KICOMMON_API wxFont GetControlFont(wxWindow *aWindow)
Definition: ui_common.cpp:168
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