/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Mark Roszko * Copyright The KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static long long g_last_closed_timer = 0; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( NOTIFICATION, title, description, href, key, date ) class NOTIFICATION_PANEL : public wxPanel { public: NOTIFICATION_PANEL( wxWindow* aParent, NOTIFICATIONS_MANAGER* aManager, NOTIFICATION* aNoti ) : wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxSize( -1, 75 ), wxBORDER_SIMPLE ), m_hlDetails( nullptr ), m_notification( aNoti ), m_manager( aManager ) { SetSizeHints( wxDefaultSize, wxDefaultSize ); wxBoxSizer* mainSizer; mainSizer = new wxBoxSizer( wxVERTICAL ); wxColour fg, bg; KIPLATFORM::UI::GetInfoBarColours( fg, bg ); SetBackgroundColour( bg ); SetForegroundColour( fg ); m_stTitle = new wxStaticText( this, wxID_ANY, aNoti->title ); m_stTitle->Wrap( -1 ); m_stTitle->SetFont( KIUI::GetControlFont( this ).Bold() ); mainSizer->Add( m_stTitle, 0, wxALL | wxEXPAND, 1 ); m_stDescription = new wxStaticText( this, wxID_ANY, aNoti->description ); m_stDescription->Wrap( -1 ); mainSizer->Add( m_stDescription, 0, wxALL | wxEXPAND, 1 ); wxBoxSizer* tailSizer; tailSizer = new wxBoxSizer( wxHORIZONTAL ); if( !aNoti->href.IsEmpty() ) { m_hlDetails = new wxHyperlinkCtrl( this, wxID_ANY, _( "View Details" ), aNoti->href ); tailSizer->Add( m_hlDetails, 0, wxALL, 2 ); } m_hlDismiss = new wxHyperlinkCtrl( this, wxID_ANY, _( "Dismiss" ), aNoti->href ); tailSizer->Add( m_hlDismiss, 0, wxALL, 2 ); mainSizer->Add( tailSizer, 1, wxEXPAND, 5 ); if( m_hlDetails != nullptr ) m_hlDetails->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDetails, this ); m_hlDismiss->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDismiss, this ); SetSizer( mainSizer ); Layout(); } private: void onDetails( wxHyperlinkEvent& aEvent ) { wxString url = aEvent.GetURL(); if( url.StartsWith( wxS( "kicad://" ) ) ) { url.Replace( wxS( "kicad://" ), wxS( "" ) ); if( url == wxS( "pcm" ) ) { // TODO } } else { wxLaunchDefaultBrowser( aEvent.GetURL(), wxBROWSER_NEW_WINDOW ); } } void onDismiss( wxHyperlinkEvent& aEvent ) { CallAfter( [this]() { // This will cause this panel to get deleted m_manager->Remove( m_notification->key ); } ); } private: wxStaticText* m_stTitle; wxStaticText* m_stDescription; wxHyperlinkCtrl* m_hlDetails; wxHyperlinkCtrl* m_hlDismiss; NOTIFICATION* m_notification; NOTIFICATIONS_MANAGER* m_manager; }; class NOTIFICATIONS_LIST : public wxFrame { public: NOTIFICATIONS_LIST( NOTIFICATIONS_MANAGER* aManager, wxWindow* parent, const wxPoint& pos ) : wxFrame( parent, wxID_ANY, _( "Notifications" ), pos, wxSize( 300, 150 ), wxFRAME_NO_TASKBAR | wxBORDER_SIMPLE ), m_manager( aManager ) { SetSizeHints( wxDefaultSize, wxDefaultSize ); wxBoxSizer* bSizer1; bSizer1 = new wxBoxSizer( wxVERTICAL ); m_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxVSCROLL | wxBORDER_SIMPLE ); wxColour fg, bg; KIPLATFORM::UI::GetInfoBarColours( fg, bg ); m_scrolledWindow->SetBackgroundColour( bg ); m_scrolledWindow->SetForegroundColour( fg ); m_scrolledWindow->SetScrollRate( 5, 5 ); m_contentSizer = new wxBoxSizer( wxVERTICAL ); m_scrolledWindow->SetSizer( m_contentSizer ); m_scrolledWindow->Layout(); m_contentSizer->Fit( m_scrolledWindow ); bSizer1->Add( m_scrolledWindow, 1, wxEXPAND | wxALL, 0 ); m_noNotificationsText = new wxStaticText( m_scrolledWindow, wxID_ANY, _( "There are no notifications available" ), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL ); m_noNotificationsText->Wrap( -1 ); m_contentSizer->Add( m_noNotificationsText, 1, wxALL | wxEXPAND, 5 ); Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this ); m_scrolledWindow->Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this ); SetSizer( bSizer1 ); Layout(); SetFocus(); } void onFocusLoss( wxFocusEvent& aEvent ) { if( IsDescendant( aEvent.GetWindow() ) ) { // Child (such as the hyperlink texts) got focus } else { Close( true ); g_last_closed_timer = wxGetLocalTimeMillis().GetValue(); } aEvent.Skip(); } void Add( NOTIFICATION* aNoti ) { m_noNotificationsText->Hide(); NOTIFICATION_PANEL* panel = new NOTIFICATION_PANEL( m_scrolledWindow, m_manager, aNoti ); m_contentSizer->Add( panel, 0, wxEXPAND | wxALL, 2 ); m_scrolledWindow->Layout(); m_contentSizer->Fit( m_scrolledWindow ); // call this at this window otherwise the child panels don't resize width properly Layout(); m_panelMap[aNoti] = panel; } void Remove( NOTIFICATION* aNoti ) { auto it = m_panelMap.find( aNoti ); if( it != m_panelMap.end() ) { NOTIFICATION_PANEL* panel = m_panelMap[aNoti]; m_contentSizer->Detach( panel ); panel->Destroy(); m_panelMap.erase( it ); // ensure the window contents get shifted as needed m_scrolledWindow->Layout(); Layout(); } if( m_panelMap.size() == 0 ) { m_noNotificationsText->Show(); } } private: wxScrolledWindow* m_scrolledWindow; /// Inner content of the scrolled window, add panels here. wxBoxSizer* m_contentSizer; std::unordered_map m_panelMap; NOTIFICATIONS_MANAGER* m_manager; /// Text to be displayed when no notifications are present, this gets a Show/Hide call as /// needed. wxStaticText* m_noNotificationsText; }; NOTIFICATIONS_MANAGER::NOTIFICATIONS_MANAGER() { m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) ); } void NOTIFICATIONS_MANAGER::Load() { nlohmann::json saved_json; std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() ); try { saved_json_stream >> saved_json; m_notifications = saved_json.get>(); } catch( std::exception& ) { // failed to load the json, which is fine, default to no notifications } if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) ) { CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ), wxS( "https://kicad.org" ) ); } } void NOTIFICATIONS_MANAGER::Save() { std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() ); nlohmann::json saveJson = nlohmann::json( m_notifications ); jsonFileStream << std::setw( 4 ) << saveJson << std::endl; jsonFileStream.flush(); jsonFileStream.close(); } void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey, const wxString& aTitle, const wxString& aDescription, const wxString& aHref ) { wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) ); auto it = std::find_if( m_notifications.begin(), m_notifications.end(), [&]( const NOTIFICATION& noti ) { return noti.key == aKey; } ); if( it != m_notifications.end() ) { NOTIFICATION& noti = *it; noti.title = aTitle; noti.description = aDescription; noti.href = aHref; } else { m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref, aKey, wxEmptyString } ); } if( m_shownDialogs.size() > 0 ) { // update dialogs for( NOTIFICATIONS_LIST* list : m_shownDialogs ) list->Add( &m_notifications.back() ); } for( KISTATUSBAR* statusBar : m_statusBars ) statusBar->SetNotificationCount( m_notifications.size() ); Save(); } void NOTIFICATIONS_MANAGER::Remove( const wxString& aKey ) { auto it = std::find_if( m_notifications.begin(), m_notifications.end(), [&]( const NOTIFICATION& noti ) { return noti.key == aKey; } ); if( it == m_notifications.end() ) return; if( m_shownDialogs.size() > 0 ) { // update dialogs for( NOTIFICATIONS_LIST* list : m_shownDialogs ) list->Remove( &(*it) ); } m_notifications.erase( it ); Save(); for( KISTATUSBAR* statusBar : m_statusBars ) statusBar->SetNotificationCount( m_notifications.size() ); } void NOTIFICATIONS_MANAGER::onListWindowClosed( wxCloseEvent& aEvent ) { NOTIFICATIONS_LIST* evtWindow = dynamic_cast( aEvent.GetEventObject() ); alg::delete_if( m_shownDialogs, [&]( NOTIFICATIONS_LIST* dialog ) { return dialog == evtWindow; } ); aEvent.Skip(); } void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos ) { // Debounce clicking on the icon with a list already showing. The button will get focus // first, which will cause a focus-loss on the list (thereby closing it), and then we'd open // it again without this guard. if( wxGetLocalTimeMillis().GetValue() - g_last_closed_timer < 300 ) { g_last_closed_timer = 0; return; } NOTIFICATIONS_LIST* list = new NOTIFICATIONS_LIST( this, aParent, aPos ); for( NOTIFICATION& job : m_notifications ) list->Add( &job ); m_shownDialogs.push_back( list ); list->Bind( wxEVT_CLOSE_WINDOW, &NOTIFICATIONS_MANAGER::onListWindowClosed, this ); // correct the position wxSize windowSize = list->GetSize(); list->SetPosition( aPos - windowSize ); list->Show(); KIPLATFORM::UI::ForceFocus( list ); } void NOTIFICATIONS_MANAGER::RegisterStatusBar( KISTATUSBAR* aStatusBar ) { m_statusBars.push_back( aStatusBar ); // notifications should already be loaded so set the initial notification count aStatusBar->SetNotificationCount( m_notifications.size() ); } void NOTIFICATIONS_MANAGER::UnregisterStatusBar( KISTATUSBAR* aStatusBar ) { alg::delete_if( m_statusBars, [&]( KISTATUSBAR* statusBar ) { return statusBar == aStatusBar; } ); }