2023-07-27 02:03:05 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2023 Mark Roszko <mark.roszko@gmail.com>
|
|
|
|
* Copyright (C) 2023 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 <wx/filename.h>
|
|
|
|
#include <wx/frame.h>
|
|
|
|
#include <wx/hyperlink.h>
|
|
|
|
#include <wx/panel.h>
|
|
|
|
#include <wx/scrolwin.h>
|
|
|
|
#include <wx/sizer.h>
|
|
|
|
#include <wx/settings.h>
|
|
|
|
#include <wx/stattext.h>
|
|
|
|
#include <wx/string.h>
|
|
|
|
|
|
|
|
#include <paths.h>
|
|
|
|
|
|
|
|
#include <notifications_manager.h>
|
|
|
|
#include <widgets/kistatusbar.h>
|
|
|
|
|
|
|
|
#include "core/wx_stl_compat.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
#include <fstream>
|
|
|
|
#include <map>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <optional>
|
|
|
|
#include <string>
|
|
|
|
#include <tuple>
|
|
|
|
#include <vector>
|
|
|
|
#include <wx/string.h>
|
|
|
|
|
|
|
|
|
|
|
|
// Teaching json en/decoder to understand wxStrings
|
|
|
|
namespace nlohmann
|
|
|
|
{
|
|
|
|
template <>
|
|
|
|
struct adl_serializer<wxString>
|
|
|
|
{
|
|
|
|
static void to_json( json& j, const wxString& s ) { j = s.ToUTF8(); }
|
|
|
|
|
|
|
|
static void from_json( const json& j, wxString& s )
|
|
|
|
{
|
|
|
|
s = wxString::FromUTF8( j.get<std::string>().c_str() );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} // namespace nlohmann
|
|
|
|
|
|
|
|
|
|
|
|
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 );
|
|
|
|
|
|
|
|
SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_3DLIGHT ) );
|
|
|
|
|
|
|
|
m_stTitle = new wxStaticText( this, wxID_ANY, aNoti->title );
|
|
|
|
m_stTitle->Wrap( -1 );
|
|
|
|
m_stTitle->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT,
|
|
|
|
wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
|
|
|
|
mainSizer->Add( m_stTitle, 0, wxALL | wxEXPAND, 1 );
|
|
|
|
|
|
|
|
m_stDescription = new wxStaticText( this, wxID_ANY, aNoti->description, wxDefaultPosition,
|
|
|
|
wxDefaultSize, 0 );
|
|
|
|
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,
|
|
|
|
wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
|
|
|
|
tailSizer->Add( m_hlDetails, 0, wxALL, 2 );
|
|
|
|
}
|
|
|
|
|
|
|
|
m_hlDismiss = new wxHyperlinkCtrl( this, wxID_ANY, _( "Dismiss" ), aNoti->href,
|
|
|
|
wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
|
|
|
|
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 will cause this panel to get deleted
|
|
|
|
m_manager->Remove( m_notification->key );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
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 ) :
|
2023-08-05 15:51:30 +00:00
|
|
|
wxFrame( parent, wxID_ANY, _( "Notifications" ), pos, wxSize( 300, 150 ),
|
|
|
|
wxFRAME_NO_TASKBAR | wxBORDER_SIMPLE ),
|
2023-07-27 02:03:05 +00:00
|
|
|
m_manager( aManager )
|
|
|
|
{
|
|
|
|
SetSizeHints( wxDefaultSize, wxDefaultSize );
|
|
|
|
|
|
|
|
wxBoxSizer* bSizer1;
|
|
|
|
bSizer1 = new wxBoxSizer( wxVERTICAL );
|
|
|
|
|
|
|
|
m_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition,
|
2023-08-05 15:51:30 +00:00
|
|
|
wxSize( -1, -1 ), wxVSCROLL );
|
2023-07-27 02:03:05 +00:00
|
|
|
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 );
|
|
|
|
|
2023-08-05 15:51:30 +00:00
|
|
|
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 );
|
|
|
|
|
2023-07-27 02:03:05 +00:00
|
|
|
Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
|
|
|
|
|
|
|
|
SetSizer( bSizer1 );
|
|
|
|
Layout();
|
|
|
|
|
|
|
|
SetFocus();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void onFocusLoss( wxFocusEvent& aEvent )
|
|
|
|
{
|
|
|
|
// check if a child like say, the hyperlink texts got focus
|
|
|
|
if( !IsDescendant( aEvent.GetWindow() ) )
|
|
|
|
Close( true );
|
|
|
|
|
|
|
|
aEvent.Skip();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Add( NOTIFICATION* aNoti )
|
|
|
|
{
|
2023-08-05 15:51:30 +00:00
|
|
|
m_noNotificationsText->Hide();
|
|
|
|
|
2023-07-27 02:03:05 +00:00
|
|
|
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 dont 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 );
|
|
|
|
}
|
2023-08-05 15:51:30 +00:00
|
|
|
|
|
|
|
if( m_panelMap.size() == 0 )
|
|
|
|
{
|
|
|
|
m_noNotificationsText->Show();
|
|
|
|
}
|
2023-07-27 02:03:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
wxScrolledWindow* m_scrolledWindow;
|
2023-08-05 15:51:30 +00:00
|
|
|
///< Inner content of the scrolled window, add panels here
|
2023-07-27 02:03:05 +00:00
|
|
|
wxBoxSizer* m_contentSizer;
|
|
|
|
std::unordered_map<NOTIFICATION*, NOTIFICATION_PANEL*> m_panelMap;
|
|
|
|
NOTIFICATIONS_MANAGER* m_manager;
|
2023-08-05 15:51:30 +00:00
|
|
|
///< Text to be displayed when no notifications are present, this gets a Show/Hide call as needed
|
|
|
|
wxStaticText* m_noNotificationsText;
|
2023-07-27 02:03:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
NOTIFICATIONS_MANAGER::NOTIFICATIONS_MANAGER()
|
|
|
|
{
|
|
|
|
m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void NOTIFICATIONS_MANAGER::Load()
|
|
|
|
{
|
|
|
|
nlohmann::json saved_json;
|
|
|
|
|
2023-08-13 18:44:31 +00:00
|
|
|
std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() );
|
2023-07-27 02:03:05 +00:00
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
saved_json_stream >> saved_json;
|
|
|
|
|
|
|
|
m_notifications = saved_json.get<std::vector<NOTIFICATION>>();
|
|
|
|
}
|
|
|
|
catch( std::exception& )
|
|
|
|
{
|
2023-08-06 20:27:27 +00:00
|
|
|
// failed to load the json, which is fine, default to no notificaitons
|
2023-07-27 02:03:05 +00:00
|
|
|
}
|
2023-08-06 18:00:55 +00:00
|
|
|
|
|
|
|
if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) )
|
|
|
|
{
|
|
|
|
CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ),
|
|
|
|
wxS( "https://kicad.org" ) );
|
|
|
|
}
|
2023-07-27 02:03:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void NOTIFICATIONS_MANAGER::Save()
|
|
|
|
{
|
2023-08-13 18:44:31 +00:00
|
|
|
std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() );
|
2023-07-27 02:03:05 +00:00
|
|
|
|
|
|
|
nlohmann::json saveJson = nlohmann::json( m_notifications );
|
|
|
|
jsonFileStream << std::setw( 4 ) << saveJson << std::endl;
|
|
|
|
jsonFileStream.flush();
|
|
|
|
jsonFileStream.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-06 18:00:55 +00:00
|
|
|
void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey,
|
2023-07-27 02:03:05 +00:00
|
|
|
const wxString& aTitle,
|
|
|
|
const wxString& aDescription,
|
|
|
|
const wxString& aHref )
|
|
|
|
{
|
2023-08-06 18:00:55 +00:00
|
|
|
wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) );
|
|
|
|
|
2023-07-27 02:03:05 +00:00
|
|
|
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
|
|
|
|
{
|
2023-08-06 11:20:43 +00:00
|
|
|
m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref,
|
|
|
|
aKey, wxEmptyString } );
|
2023-07-27 02:03:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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<NOTIFICATIONS_LIST*>( aEvent.GetEventObject() );
|
|
|
|
|
|
|
|
m_shownDialogs.erase( std::remove_if( m_shownDialogs.begin(), m_shownDialogs.end(),
|
|
|
|
[&]( NOTIFICATIONS_LIST* dialog )
|
|
|
|
{
|
|
|
|
return dialog == evtWindow;
|
|
|
|
} ) );
|
|
|
|
|
|
|
|
aEvent.Skip();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos )
|
|
|
|
{
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 )
|
|
|
|
{
|
|
|
|
m_statusBars.erase( std::remove_if( m_statusBars.begin(), m_statusBars.end(),
|
|
|
|
[&]( KISTATUSBAR* statusBar )
|
|
|
|
{
|
|
|
|
return statusBar == aStatusBar;
|
|
|
|
} ) );
|
|
|
|
}
|