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.

420 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 <json_common.h>
  37. #include "core/wx_stl_compat.h"
  38. #include <algorithm>
  39. #include <fstream>
  40. #include <map>
  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. [this]()
  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. m_scrolledWindow->Bind( wxEVT_KILL_FOCUS, &NOTIFICATIONS_LIST::onFocusLoss, this );
  151. SetSizer( bSizer1 );
  152. Layout();
  153. SetFocus();
  154. }
  155. void onFocusLoss( wxFocusEvent& aEvent )
  156. {
  157. // check if a child like say, the hyperlink texts got focus
  158. if( !IsDescendant( aEvent.GetWindow() ) )
  159. Close( true );
  160. aEvent.Skip();
  161. }
  162. void Add( NOTIFICATION* aNoti )
  163. {
  164. m_noNotificationsText->Hide();
  165. NOTIFICATION_PANEL* panel = new NOTIFICATION_PANEL( m_scrolledWindow, m_manager, aNoti );
  166. m_contentSizer->Add( panel, 0, wxEXPAND | wxALL, 2 );
  167. m_scrolledWindow->Layout();
  168. m_contentSizer->Fit( m_scrolledWindow );
  169. // call this at this window otherwise the child panels dont resize width properly
  170. Layout();
  171. m_panelMap[aNoti] = panel;
  172. }
  173. void Remove( NOTIFICATION* aNoti )
  174. {
  175. auto it = m_panelMap.find( aNoti );
  176. if( it != m_panelMap.end() )
  177. {
  178. NOTIFICATION_PANEL* panel = m_panelMap[aNoti];
  179. m_contentSizer->Detach( panel );
  180. panel->Destroy();
  181. m_panelMap.erase( it );
  182. // ensure the window contents get shifted as needed
  183. m_scrolledWindow->Layout();
  184. Layout();
  185. }
  186. if( m_panelMap.size() == 0 )
  187. {
  188. m_noNotificationsText->Show();
  189. }
  190. }
  191. private:
  192. wxScrolledWindow* m_scrolledWindow;
  193. ///< Inner content of the scrolled window, add panels here
  194. wxBoxSizer* m_contentSizer;
  195. std::unordered_map<NOTIFICATION*, NOTIFICATION_PANEL*> m_panelMap;
  196. NOTIFICATIONS_MANAGER* m_manager;
  197. ///< Text to be displayed when no notifications are present, this gets a Show/Hide call as needed
  198. wxStaticText* m_noNotificationsText;
  199. };
  200. NOTIFICATIONS_MANAGER::NOTIFICATIONS_MANAGER()
  201. {
  202. m_destFileName = wxFileName( PATHS::GetUserCachePath(), wxT( "notifications.json" ) );
  203. }
  204. void NOTIFICATIONS_MANAGER::Load()
  205. {
  206. nlohmann::json saved_json;
  207. std::ifstream saved_json_stream( m_destFileName.GetFullPath().fn_str() );
  208. try
  209. {
  210. saved_json_stream >> saved_json;
  211. m_notifications = saved_json.get<std::vector<NOTIFICATION>>();
  212. }
  213. catch( std::exception& )
  214. {
  215. // failed to load the json, which is fine, default to no notificaitons
  216. }
  217. if( wxGetEnv( wxT( "KICAD_TEST_NOTI" ), nullptr ) )
  218. {
  219. CreateOrUpdate( wxS( "test" ), wxS( "Test Notification" ), wxS( "Test please ignore" ),
  220. wxS( "https://kicad.org" ) );
  221. }
  222. }
  223. void NOTIFICATIONS_MANAGER::Save()
  224. {
  225. std::ofstream jsonFileStream( m_destFileName.GetFullPath().fn_str() );
  226. nlohmann::json saveJson = nlohmann::json( m_notifications );
  227. jsonFileStream << std::setw( 4 ) << saveJson << std::endl;
  228. jsonFileStream.flush();
  229. jsonFileStream.close();
  230. }
  231. void NOTIFICATIONS_MANAGER::CreateOrUpdate( const wxString& aKey,
  232. const wxString& aTitle,
  233. const wxString& aDescription,
  234. const wxString& aHref )
  235. {
  236. wxCHECK_RET( !aKey.IsEmpty(), wxS( "Notification key must not be empty" ) );
  237. auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
  238. [&]( const NOTIFICATION& noti )
  239. {
  240. return noti.key == aKey;
  241. } );
  242. if( it != m_notifications.end() )
  243. {
  244. NOTIFICATION& noti = *it;
  245. noti.title = aTitle;
  246. noti.description = aDescription;
  247. noti.href = aHref;
  248. }
  249. else
  250. {
  251. m_notifications.emplace_back( NOTIFICATION{ aTitle, aDescription, aHref,
  252. aKey, wxEmptyString } );
  253. }
  254. if( m_shownDialogs.size() > 0 )
  255. {
  256. // update dialogs
  257. for( NOTIFICATIONS_LIST* list : m_shownDialogs )
  258. {
  259. list->Add( &m_notifications.back() );
  260. }
  261. }
  262. for( KISTATUSBAR* statusBar : m_statusBars )
  263. {
  264. statusBar->SetNotificationCount( m_notifications.size() );
  265. }
  266. Save();
  267. }
  268. void NOTIFICATIONS_MANAGER::Remove( const wxString& aKey )
  269. {
  270. auto it = std::find_if( m_notifications.begin(), m_notifications.end(),
  271. [&]( const NOTIFICATION& noti )
  272. {
  273. return noti.key == aKey;
  274. } );
  275. if( it == m_notifications.end() )
  276. {
  277. return;
  278. }
  279. if( m_shownDialogs.size() > 0 )
  280. {
  281. // update dialogs
  282. for( NOTIFICATIONS_LIST* list : m_shownDialogs )
  283. {
  284. list->Remove( &(*it) );
  285. }
  286. }
  287. m_notifications.erase( it );
  288. Save();
  289. for( KISTATUSBAR* statusBar : m_statusBars )
  290. {
  291. statusBar->SetNotificationCount( m_notifications.size() );
  292. }
  293. }
  294. void NOTIFICATIONS_MANAGER::onListWindowClosed( wxCloseEvent& aEvent )
  295. {
  296. NOTIFICATIONS_LIST* evtWindow = dynamic_cast<NOTIFICATIONS_LIST*>( aEvent.GetEventObject() );
  297. m_shownDialogs.erase( std::remove_if( m_shownDialogs.begin(), m_shownDialogs.end(),
  298. [&]( NOTIFICATIONS_LIST* dialog )
  299. {
  300. return dialog == evtWindow;
  301. } ) );
  302. aEvent.Skip();
  303. }
  304. void NOTIFICATIONS_MANAGER::ShowList( wxWindow* aParent, wxPoint aPos )
  305. {
  306. NOTIFICATIONS_LIST* list = new NOTIFICATIONS_LIST( this, aParent, aPos );
  307. for( NOTIFICATION& job : m_notifications )
  308. {
  309. list->Add( &job );
  310. }
  311. m_shownDialogs.push_back( list );
  312. list->Bind( wxEVT_CLOSE_WINDOW, &NOTIFICATIONS_MANAGER::onListWindowClosed, this );
  313. // correct the position
  314. wxSize windowSize = list->GetSize();
  315. list->SetPosition( aPos - windowSize );
  316. list->Show();
  317. }
  318. void NOTIFICATIONS_MANAGER::RegisterStatusBar( KISTATUSBAR* aStatusBar )
  319. {
  320. m_statusBars.push_back( aStatusBar );
  321. // notifications should already be loaded so set the initial notification count
  322. aStatusBar->SetNotificationCount( m_notifications.size() );
  323. }
  324. void NOTIFICATIONS_MANAGER::UnregisterStatusBar( KISTATUSBAR* aStatusBar )
  325. {
  326. m_statusBars.erase( std::remove_if( m_statusBars.begin(), m_statusBars.end(),
  327. [&]( KISTATUSBAR* statusBar )
  328. {
  329. return statusBar == aStatusBar;
  330. } ) );
  331. }