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