You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
13 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2023 Mark Roszko <mark.roszko@gmail.com>
  5. * Copyright (C) 2023 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. #include <wx/filename.h>
  25. #include <wx/frame.h>
  26. #include <wx/hyperlink.h>
  27. #include <wx/panel.h>
  28. #include <wx/scrolwin.h>
  29. #include <wx/sizer.h>
  30. #include <wx/settings.h>
  31. #include <wx/stattext.h>
  32. #include <wx/string.h>
  33. #include <paths.h>
  34. #include <notifications_manager.h>
  35. #include <widgets/kistatusbar.h>
  36. #include "core/wx_stl_compat.h"
  37. #include <algorithm>
  38. #include <fstream>
  39. #include <map>
  40. #include <nlohmann/json.hpp>
  41. #include <core/json_serializers.h>
  42. #include <optional>
  43. #include <string>
  44. #include <tuple>
  45. #include <vector>
  46. #include <wx/string.h>
  47. NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( NOTIFICATION, title, description, href, key, date )
  48. class NOTIFICATION_PANEL : public wxPanel
  49. {
  50. public:
  51. NOTIFICATION_PANEL( wxWindow* aParent, NOTIFICATIONS_MANAGER* aManager, NOTIFICATION* aNoti ) :
  52. wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxSize( -1, 75 ),
  53. wxBORDER_SIMPLE ),
  54. m_hlDetails( nullptr ),
  55. m_notification( aNoti ),
  56. m_manager( aManager )
  57. {
  58. SetSizeHints( wxDefaultSize, wxDefaultSize );
  59. wxBoxSizer* mainSizer;
  60. mainSizer = new wxBoxSizer( wxVERTICAL );
  61. SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_3DLIGHT ) );
  62. m_stTitle = new wxStaticText( this, wxID_ANY, aNoti->title );
  63. m_stTitle->Wrap( -1 );
  64. m_stTitle->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT,
  65. wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
  66. mainSizer->Add( m_stTitle, 0, wxALL | wxEXPAND, 1 );
  67. m_stDescription = new wxStaticText( this, wxID_ANY, aNoti->description, wxDefaultPosition,
  68. wxDefaultSize, 0 );
  69. m_stDescription->Wrap( -1 );
  70. mainSizer->Add( m_stDescription, 0, wxALL | wxEXPAND, 1 );
  71. wxBoxSizer* tailSizer;
  72. tailSizer = new wxBoxSizer( wxHORIZONTAL );
  73. if( !aNoti->href.IsEmpty() )
  74. {
  75. m_hlDetails =
  76. new wxHyperlinkCtrl( this, wxID_ANY, _( "View Details" ), aNoti->href,
  77. wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
  78. tailSizer->Add( m_hlDetails, 0, wxALL, 2 );
  79. }
  80. m_hlDismiss = new wxHyperlinkCtrl( this, wxID_ANY, _( "Dismiss" ), aNoti->href,
  81. wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
  82. tailSizer->Add( m_hlDismiss, 0, wxALL, 2 );
  83. mainSizer->Add( tailSizer, 1, wxEXPAND, 5 );
  84. if( m_hlDetails != nullptr )
  85. {
  86. m_hlDetails->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDetails, this );
  87. }
  88. m_hlDismiss->Bind( wxEVT_HYPERLINK, &NOTIFICATION_PANEL::onDismiss, this );
  89. SetSizer( mainSizer );
  90. Layout();
  91. }
  92. private:
  93. void onDetails( wxHyperlinkEvent& aEvent )
  94. {
  95. wxString url = aEvent.GetURL();
  96. if( url.StartsWith( wxS( "kicad://" ) ) )
  97. {
  98. url.Replace( wxS( "kicad://" ), wxS( "" ) );
  99. if( url == wxS( "pcm" ) )
  100. {
  101. // TODO
  102. }
  103. }
  104. else
  105. {
  106. wxLaunchDefaultBrowser( aEvent.GetURL(), wxBROWSER_NEW_WINDOW );
  107. }
  108. }
  109. void onDismiss( wxHyperlinkEvent& aEvent )
  110. {
  111. CallAfter(
  112. [=]()
  113. {
  114. // This will cause this panel to get deleted
  115. m_manager->Remove( m_notification->key );
  116. } );
  117. }
  118. wxStaticText* m_stTitle;
  119. wxStaticText* m_stDescription;
  120. wxHyperlinkCtrl* m_hlDetails;
  121. wxHyperlinkCtrl* m_hlDismiss;
  122. NOTIFICATION* m_notification;
  123. NOTIFICATIONS_MANAGER* m_manager;
  124. };
  125. class NOTIFICATIONS_LIST : public wxFrame
  126. {
  127. public:
  128. NOTIFICATIONS_LIST( NOTIFICATIONS_MANAGER* aManager, wxWindow* parent, const wxPoint& pos ) :
  129. wxFrame( parent, wxID_ANY, _( "Notifications" ), pos, wxSize( 300, 150 ),
  130. wxFRAME_NO_TASKBAR | wxBORDER_SIMPLE ),
  131. m_manager( aManager )
  132. {
  133. SetSizeHints( wxDefaultSize, wxDefaultSize );
  134. wxBoxSizer* bSizer1;
  135. bSizer1 = new wxBoxSizer( wxVERTICAL );
  136. m_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition,
  137. wxSize( -1, -1 ), wxVSCROLL );
  138. m_scrolledWindow->SetScrollRate( 5, 5 );
  139. m_contentSizer = new wxBoxSizer( wxVERTICAL );
  140. m_scrolledWindow->SetSizer( m_contentSizer );
  141. m_scrolledWindow->Layout();
  142. m_contentSizer->Fit( m_scrolledWindow );
  143. bSizer1->Add( m_scrolledWindow, 1, wxEXPAND | wxALL, 0 );
  144. m_noNotificationsText = new wxStaticText(
  145. m_scrolledWindow, wxID_ANY, _( "There are no notifications available" ),
  146. wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL );
  147. m_noNotificationsText->Wrap( -1 );
  148. m_contentSizer->Add( m_noNotificationsText, 1, wxALL | wxEXPAND, 5 );
  149. Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
  150. SetSizer( bSizer1 );
  151. Layout();
  152. SetFocus();
  153. }
  154. void onFocusLoss( wxFocusEvent& aEvent )
  155. {
  156. // check if a child like say, the hyperlink texts got focus
  157. if( !IsDescendant( aEvent.GetWindow() ) )
  158. Close( true );
  159. aEvent.Skip();
  160. }
  161. void Add( NOTIFICATION* aNoti )
  162. {
  163. m_noNotificationsText->Hide();
  164. NOTIFICATION_PANEL* panel = new NOTIFICATION_PANEL( m_scrolledWindow, m_manager, aNoti );
  165. m_contentSizer->Add( panel, 0, wxEXPAND | wxALL, 2 );
  166. m_scrolledWindow->Layout();
  167. m_contentSizer->Fit( m_scrolledWindow );
  168. // call this at this window otherwise the child panels dont resize width properly
  169. Layout();
  170. m_panelMap[aNoti] = panel;
  171. }
  172. void Remove( NOTIFICATION* aNoti )
  173. {
  174. auto it = m_panelMap.find( aNoti );
  175. if( it != m_panelMap.end() )
  176. {
  177. NOTIFICATION_PANEL* panel = m_panelMap[aNoti];
  178. m_contentSizer->Detach( panel );
  179. panel->Destroy();
  180. m_panelMap.erase( it );
  181. // ensure the window contents get shifted as needed
  182. m_scrolledWindow->Layout();
  183. Layout();
  184. }
  185. if( m_panelMap.size() == 0 )
  186. {
  187. m_noNotificationsText->Show();
  188. }
  189. }
  190. private:
  191. wxScrolledWindow* m_scrolledWindow;
  192. ///< Inner content of the scrolled window, add panels here
  193. wxBoxSizer* m_contentSizer;
  194. std::unordered_map<NOTIFICATION*, NOTIFICATION_PANEL*> m_panelMap;
  195. NOTIFICATIONS_MANAGER* m_manager;
  196. ///< Text to be displayed when no notifications are present, this gets a Show/Hide call as needed
  197. wxStaticText* m_noNotificationsText;
  198. };
  199. NOTIFICATIONS_MANAGER::NOTIFICATIONS_MANAGER()
  200. {
  201. m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) );
  202. }
  203. void NOTIFICATIONS_MANAGER::Load()
  204. {
  205. nlohmann::json saved_json;
  206. std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() );
  207. try
  208. {
  209. saved_json_stream >> saved_json;
  210. m_notifications = saved_json.get<std::vector<NOTIFICATION>>();
  211. }
  212. catch( std::exception& )
  213. {
  214. // failed to load the json, which is fine, default to no notificaitons
  215. }
  216. if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) )
  217. {
  218. CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ),
  219. wxS( "https://kicad.org" ) );
  220. }
  221. }
  222. void NOTIFICATIONS_MANAGER::Save()
  223. {
  224. std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() );
  225. nlohmann::json saveJson = nlohmann::json( m_notifications );
  226. jsonFileStream << std::setw( 4 ) << saveJson << std::endl;
  227. jsonFileStream.flush();
  228. jsonFileStream.close();
  229. }
  230. void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey,
  231. const wxString& aTitle,
  232. const wxString& aDescription,
  233. const wxString& aHref )
  234. {
  235. wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) );
  236. auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
  237. [&]( const NOTIFICATION& noti )
  238. {
  239. return noti.key == aKey;
  240. } );
  241. if( it != m_notifications.end() )
  242. {
  243. NOTIFICATION& noti = *it;
  244. noti.title = aTitle;
  245. noti.description = aDescription;
  246. noti.href = aHref;
  247. }
  248. else
  249. {
  250. m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref,
  251. aKey, wxEmptyString } );
  252. }
  253. if( m_shownDialogs.size() > 0 )
  254. {
  255. // update dialogs
  256. for( NOTIFICATIONS_LIST* list : m_shownDialogs )
  257. {
  258. list->Add( &m_notifications.back() );
  259. }
  260. }
  261. for( KISTATUSBAR* statusBar : m_statusBars )
  262. {
  263. statusBar->SetNotificationCount( m_notifications.size() );
  264. }
  265. Save();
  266. }
  267. void NOTIFICATIONS_MANAGER::Remove( const wxString& aKey )
  268. {
  269. auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
  270. [&]( const NOTIFICATION& noti )
  271. {
  272. return noti.key == aKey;
  273. } );
  274. if( it == m_notifications.end() )
  275. {
  276. return;
  277. }
  278. if( m_shownDialogs.size() > 0 )
  279. {
  280. // update dialogs
  281. for( NOTIFICATIONS_LIST* list : m_shownDialogs )
  282. {
  283. list->Remove( &(*it) );
  284. }
  285. }
  286. m_notifications.erase( it );
  287. Save();
  288. for( KISTATUSBAR* statusBar : m_statusBars )
  289. {
  290. statusBar->SetNotificationCount( m_notifications.size() );
  291. }
  292. }
  293. void NOTIFICATIONS_MANAGER::onListWindowClosed( wxCloseEvent& aEvent )
  294. {
  295. NOTIFICATIONS_LIST* evtWindow = dynamic_cast<NOTIFICATIONS_LIST*>( aEvent.GetEventObject() );
  296. m_shownDialogs.erase( std::remove_if( m_shownDialogs.begin(), m_shownDialogs.end(),
  297. [&]( NOTIFICATIONS_LIST* dialog )
  298. {
  299. return dialog == evtWindow;
  300. } ) );
  301. aEvent.Skip();
  302. }
  303. void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos )
  304. {
  305. NOTIFICATIONS_LIST* list = new NOTIFICATIONS_LIST( this, aParent, aPos );
  306. for( NOTIFICATION& job : m_notifications )
  307. {
  308. list->Add( &job );
  309. }
  310. m_shownDialogs.push_back( list );
  311. list->Bind( wxEVT_CLOSE_WINDOW, &NOTIFICATIONS_MANAGER::onListWindowClosed, this );
  312. // correct the position
  313. wxSize windowSize = list->GetSize();
  314. list->SetPosition( aPos - windowSize );
  315. list->Show();
  316. }
  317. void NOTIFICATIONS_MANAGER::RegisterStatusBar( KISTATUSBAR* aStatusBar )
  318. {
  319. m_statusBars.push_back( aStatusBar );
  320. // notifications should already be loaded so set the initial notification count
  321. aStatusBar->SetNotificationCount( m_notifications.size() );
  322. }
  323. void NOTIFICATIONS_MANAGER::UnregisterStatusBar( KISTATUSBAR* aStatusBar )
  324. {
  325. m_statusBars.erase( std::remove_if( m_statusBars.begin(), m_statusBars.end(),
  326. [&]( KISTATUSBAR* statusBar )
  327. {
  328. return statusBar == aStatusBar;
  329. } ) );
  330. }