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.

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