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.

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