KiCad PCB EDA Suite
panel_common_settings.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2018-2021 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <advanced_config.h>
27#include <bitmaps.h>
28#include <dialog_shim.h>
29#include <gal/dpi_scaling.h>
30#include <kiface_base.h>
31#include <kiplatform/ui.h>
32#include <pgm_base.h>
33#include <id.h>
37#include <wx/filedlg.h>
38
39/*
40 * What follows is a whole lot of ugly to handle various platform GUI deficiences with respect
41 * to light/dark mode, DPI scaling, and other foibles.
42 *
43 * Ugly as it all is, it does improve our usability on various platforms.
44 */
45
47 : PANEL_COMMON_SETTINGS_BASE( aParent ),
48 m_dialog( aDialog ),
49 m_iconScaleLabel( nullptr ),
50 m_iconScaleSlider( nullptr ),
51 m_iconScaleAuto( nullptr ),
52 m_last_scale( -1 )
53{
54 /*
55 * Cairo canvas doesn't work on Mac, so no need for fallback anti-aliasing options
56 */
57#ifdef __WXMAC__
58 m_antialiasingFallback->Show( false );
59 m_antialiasingFallbackLabel->Show( false );
60#endif
61
64
65 /*
66 * Automatic dark mode detection works fine on Mac, so no need for the explicit options.
67 */
68#ifdef __WXMAC__
69 m_stIconTheme->Show( false );
70 m_rbIconThemeLight->Show( false );
71 m_rbIconThemeDark->Show( false );
72 m_rbIconThemeAuto->Show( false );
73#endif
74
75 /*
76 * Automatic icon scaling works fine on Mac. It works mostly fine on MSW, but perhaps not
77 * uniformly enough to exclude the explicit controls there.
78 */
79#if defined( __WXGTK__ ) || defined( __WXMSW__ )
80 // Sadly wxSlider is poorly implemented and adds its legends as sibling windows (so that
81 // showing/hiding the control doesn't work). So we have to create it conditionally.
82 wxWindow* parent = m_sbUserInterface->GetStaticBox();
83 wxGridBagSizer* gb = m_gbUserInterface;
84
85 m_iconScaleLabel = new wxStaticText( parent, wxID_ANY, _( "Icon scale:" ) );
86 m_iconScaleLabel->Wrap( -1 );
87 gb->Add( m_iconScaleLabel, wxGBPosition( 3, 0 ),
88 wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL, 5 );
89
90 m_iconScaleSlider = new STEPPED_SLIDER( parent, wxID_ANY, 100, 50, 275, wxDefaultPosition,
91 wxDefaultSize, wxSL_HORIZONTAL|wxSL_VALUE_LABEL );
93 gb->Add( m_iconScaleSlider, wxGBPosition( 3, 1 ), wxGBSpan( 1, 2 ), wxEXPAND|wxBOTTOM, 5 );
94
95 m_iconScaleAuto = new wxCheckBox( parent, wxID_ANY, _( "Automatic" ) );
96 gb->Add( m_iconScaleAuto, wxGBPosition( 3, 3 ),
97 wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxLEFT, 15 );
98#endif
99
100 /*
101 * Automatic canvas scaling works fine on all supported platforms, so manual scaling is disabled
102 */
103 if( ADVANCED_CFG::GetCfg().m_AllowManualCanvasScale )
104 {
105 static constexpr int dpi_scaling_precision = 1;
106 static constexpr double dpi_scaling_increment = 0.5;
107
110 m_canvasScaleCtrl->SetDigits( dpi_scaling_precision );
111 m_canvasScaleCtrl->SetIncrement( dpi_scaling_increment );
113
114 m_canvasScaleCtrl->SetToolTip(
115 _( "Set the scale for the canvas."
116 "\n\n"
117 "On high-DPI displays on some platforms, KiCad cannot determine the "
118 "scaling factor. In this case you may need to set this to a value to "
119 "match your system's DPI scaling. 2.0 is a common value. "
120 "\n\n"
121 "If this does not match the system DPI scaling, the canvas will "
122 "not match the window size and cursor position." ) );
123
124 m_canvasScaleAuto->SetToolTip(
125 _( "Use an automatic value for the canvas scale."
126 "\n\n"
127 "On some platforms, the automatic value is incorrect and should be "
128 "set manually." ) );
129 }
130 else
131 {
132 m_staticTextCanvasScale->Show( false );
133 m_canvasScaleCtrl->Show( false );
134 m_canvasScaleCtrl = nullptr;
135 m_canvasScaleAuto->Show( false );
136 }
137
138 // Hide the option of icons in menus for platforms that do not support them
140
141 /*
142 * Font scaling hacks are only needed on GTK under wxWidgets 3.0.
143 */
144#if defined( __WXGTK__ ) && !wxCHECK_VERSION( 3, 1, 0 )
145 m_fontScalingHelp->SetFont( KIUI::GetInfoFont( this ).Italic() );
146#else
147 m_scaleFonts->Show( false );
148 m_fontScalingHelp->Show( false );
149#endif
150
152 {
153 m_iconScaleSlider->Connect( wxEVT_SCROLL_TOP,
154 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
155 nullptr, this );
156 m_iconScaleSlider->Connect( wxEVT_SCROLL_BOTTOM,
157 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
158 nullptr, this );
159 m_iconScaleSlider->Connect( wxEVT_SCROLL_LINEUP,
160 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
161 nullptr, this );
162 m_iconScaleSlider->Connect( wxEVT_SCROLL_LINEDOWN,
163 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
164 nullptr, this );
165 m_iconScaleSlider->Connect( wxEVT_SCROLL_PAGEUP,
166 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
167 nullptr, this );
168 m_iconScaleSlider->Connect( wxEVT_SCROLL_PAGEDOWN,
169 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
170 nullptr, this );
171 m_iconScaleSlider->Connect( wxEVT_SCROLL_THUMBTRACK,
172 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
173 nullptr, this );
174 m_iconScaleSlider->Connect( wxEVT_SCROLL_THUMBRELEASE,
175 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
176 nullptr, this );
177 m_iconScaleSlider->Connect( wxEVT_SCROLL_CHANGED,
178 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
179 nullptr, this );
180 m_iconScaleAuto->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED,
181 wxCommandEventHandler( PANEL_COMMON_SETTINGS::OnIconScaleAuto ),
182 nullptr, this );
183 }
184
186 {
187 m_canvasScaleCtrl->Connect( wxEVT_COMMAND_TEXT_UPDATED,
188 wxCommandEventHandler( PANEL_COMMON_SETTINGS::OnCanvasScaleChange ),
189 nullptr, this );
190 }
191}
192
193
195{
197 {
198 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_TOP,
199 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
200 nullptr, this );
201 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_BOTTOM,
202 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
203 nullptr, this );
204 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_LINEUP,
205 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
206 nullptr, this );
207 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_LINEDOWN,
208 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
209 nullptr, this );
210 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_PAGEUP,
211 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
212 nullptr, this );
213 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_PAGEDOWN,
214 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
215 nullptr, this );
216 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_THUMBTRACK,
217 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
218 nullptr, this );
219 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_THUMBRELEASE,
220 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
221 nullptr, this );
222 m_iconScaleSlider->Disconnect( wxEVT_SCROLL_CHANGED,
223 wxScrollEventHandler( PANEL_COMMON_SETTINGS::OnScaleSlider ),
224 nullptr, this );
225 m_iconScaleAuto->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED,
226 wxCommandEventHandler( PANEL_COMMON_SETTINGS::OnIconScaleAuto ),
227 nullptr, this );
228 }
229
231 {
232 m_canvasScaleCtrl->Disconnect( wxEVT_COMMAND_TEXT_UPDATED,
233 wxCommandEventHandler( PANEL_COMMON_SETTINGS::OnCanvasScaleChange ),
234 nullptr, this );
235 }
236}
237
238
240{
241 COMMON_SETTINGS* commonSettings = Pgm().GetCommonSettings();
242
243 applySettingsToPanel( *commonSettings );
244
245 // TODO(JE) Move these into COMMON_SETTINGS probably
246 m_textEditorPath->SetValue( Pgm().GetTextEditor( false ) );
247 m_defaultPDFViewer->SetValue( Pgm().UseSystemPdfBrowser() );
248 m_otherPDFViewer->SetValue( !Pgm().UseSystemPdfBrowser() );
249 m_PDFViewerPath->SetValue( Pgm().GetPdfBrowserName() );
251
252 return true;
253}
254
255
257{
258 COMMON_SETTINGS* commonSettings = Pgm().GetCommonSettings();
259
260 commonSettings->m_System.autosave_interval = m_SaveTime->GetValue() * 60;
261 commonSettings->m_System.file_history_size = m_fileHistorySize->GetValue();
262 commonSettings->m_System.clear_3d_cache_interval = m_Clear3DCacheFilesOlder->GetValue();
263
264 commonSettings->m_Graphics.opengl_aa_mode = m_antialiasing->GetSelection();
265 commonSettings->m_Graphics.cairo_aa_mode = m_antialiasingFallback->GetSelection();
266
268 {
269 int scale_fourths = m_iconScaleAuto->GetValue() ? -1 : m_iconScaleSlider->GetValue() / 25;
270 commonSettings->m_Appearance.icon_scale = scale_fourths;
271 }
272
274 {
275 DPI_SCALING dpi( commonSettings, this );
276 dpi.SetDpiConfig( m_canvasScaleAuto->GetValue(), m_canvasScaleCtrl->GetValue() );
277 }
278
279 if( m_rbIconThemeLight->GetValue() )
280 commonSettings->m_Appearance.icon_theme = ICON_THEME::LIGHT;
281 else if( m_rbIconThemeDark->GetValue() )
282 commonSettings->m_Appearance.icon_theme = ICON_THEME::DARK;
283 else if( m_rbIconThemeAuto->GetValue() )
284 commonSettings->m_Appearance.icon_theme = ICON_THEME::AUTO;
285
286 commonSettings->m_Appearance.use_icons_in_menus = m_checkBoxIconsInMenus->GetValue();
287 commonSettings->m_Appearance.apply_icon_scale_to_fonts = m_scaleFonts->GetValue();
288
289 commonSettings->m_Appearance.show_scrollbars = m_showScrollbars->GetValue();
290
291 double dimmingPercent = 80;
292 m_highContrastCtrl->GetValue().ToDouble( &dimmingPercent );
293 commonSettings->m_Appearance.hicontrast_dimming_factor = dimmingPercent / 100.0f;
294
295 commonSettings->m_Input.focus_follow_sch_pcb = m_focusFollowSchPcb->GetValue();
296 commonSettings->m_Input.immediate_actions = !m_NonImmediateActions->GetValue();
297 commonSettings->m_Input.warp_mouse_on_move = m_warpMouseOnMove->GetValue();
298
299 commonSettings->m_Backup.enabled = m_cbBackupEnabled->GetValue();
300 commonSettings->m_Backup.backup_on_autosave = m_cbBackupAutosave->GetValue();
301 commonSettings->m_Backup.limit_total_files = m_backupLimitTotalFiles->GetValue();
302 commonSettings->m_Backup.limit_daily_files = m_backupLimitDailyFiles->GetValue();
303 commonSettings->m_Backup.min_interval = m_backupMinInterval->GetValue() * 60;
304 commonSettings->m_Backup.limit_total_size = m_backupLimitTotalSize->GetValue() * 1024 * 1024;
305
306 commonSettings->m_Session.remember_open_files = m_cbRememberOpenFiles->GetValue();
307
308 Pgm().SetTextEditor( m_textEditorPath->GetValue());
309
310 Pgm().SetPdfBrowserName( m_PDFViewerPath->GetValue() );
311 Pgm().ForceSystemPdfBrowser( m_defaultPDFViewer->GetValue() );
312 Pgm().WritePdfBrowserInfos();
313
314 Pgm().GetSettingsManager().Save( commonSettings );
315
316 return true;
317}
318
319
321{
322 COMMON_SETTINGS defaultSettings;
323
324 defaultSettings.ResetToDefaults();
325
326 applySettingsToPanel( defaultSettings );
327
328 // TODO(JE) Move these into COMMON_SETTINGS probably
329 m_textEditorPath->SetValue( defaultSettings.m_System.text_editor );
330 m_defaultPDFViewer->SetValue( defaultSettings.m_System.use_system_pdf_viewer );
331 m_otherPDFViewer->SetValue( !defaultSettings.m_System.use_system_pdf_viewer );
332 m_PDFViewerPath->SetValue( defaultSettings.m_System.pdf_viewer_name );
334}
335
336
338{
339 int timevalue = aSettings.m_System.autosave_interval;
340 wxString msg;
341
342 msg << timevalue / 60;
343 m_SaveTime->SetValue( msg );
344
345 m_fileHistorySize->SetValue( aSettings.m_System.file_history_size );
346
347 m_antialiasing->SetSelection( aSettings.m_Graphics.opengl_aa_mode );
348 m_antialiasingFallback->SetSelection( aSettings.m_Graphics.cairo_aa_mode );
349
351
353 {
354 int icon_scale_fourths = aSettings.m_Appearance.icon_scale;
355
356 if( icon_scale_fourths <= 0 )
357 {
358 m_iconScaleAuto->SetValue( true );
359 m_iconScaleSlider->SetValue( 25 * KiIconScale( GetParent() ) );
360 }
361 else
362 {
363 m_iconScaleAuto->SetValue( false );
364 m_iconScaleSlider->SetValue( icon_scale_fourths * 25 );
365 }
366 }
367
369 {
370 const DPI_SCALING dpi( &aSettings, this );
371 m_canvasScaleCtrl->SetValue( dpi.GetScaleFactor() );
372 m_canvasScaleAuto->SetValue( dpi.GetCanvasIsAutoScaled() );
373 }
374
375 switch( aSettings.m_Appearance.icon_theme )
376 {
377 case ICON_THEME::LIGHT: m_rbIconThemeLight->SetValue( true ); break;
378 case ICON_THEME::DARK: m_rbIconThemeDark->SetValue( true ); break;
379 case ICON_THEME::AUTO: m_rbIconThemeAuto->SetValue( true ); break;
380 }
381
384
385 double dimmingPercent = aSettings.m_Appearance.hicontrast_dimming_factor * 100.0f;
386 m_highContrastCtrl->SetValue( wxString::Format( "%.0f", dimmingPercent ) );
387
388 m_focusFollowSchPcb->SetValue( aSettings.m_Input.focus_follow_sch_pcb );
389 m_warpMouseOnMove->SetValue( aSettings.m_Input.warp_mouse_on_move );
390 m_NonImmediateActions->SetValue( !aSettings.m_Input.immediate_actions );
391
393
394 m_cbBackupEnabled->SetValue( aSettings.m_Backup.enabled );
395 m_cbBackupAutosave->SetValue( aSettings.m_Backup.backup_on_autosave );
398 m_backupMinInterval->SetValue( aSettings.m_Backup.min_interval / 60 );
399 m_backupLimitTotalSize->SetValue( aSettings.m_Backup.limit_total_size / ( 1024 * 1024 ) );
400
401 m_showScrollbars->SetValue( aSettings.m_Appearance.show_scrollbars );
402}
403
404
405void PANEL_COMMON_SETTINGS::OnScaleSlider( wxScrollEvent& aEvent )
406{
407 m_iconScaleAuto->SetValue( false );
408 aEvent.Skip();
409}
410
411
412void PANEL_COMMON_SETTINGS::OnIconScaleAuto( wxCommandEvent& aEvent )
413{
415 {
416 if( m_iconScaleAuto->GetValue() )
417 {
418 m_last_scale = m_iconScaleAuto->GetValue();
419 m_iconScaleSlider->SetValue( 25 * KiIconScale( GetParent() ) );
420 }
421 else
422 {
423 if( m_last_scale >= 0 )
424 m_iconScaleSlider->SetValue( m_last_scale );
425 }
426 }
427}
428
429
430void PANEL_COMMON_SETTINGS::OnCanvasScaleChange( wxCommandEvent& aEvent )
431{
432 m_canvasScaleAuto->SetValue( false );
433}
434
435
436void PANEL_COMMON_SETTINGS::OnCanvasScaleAuto( wxCommandEvent& aEvent )
437{
438 const bool automatic = m_canvasScaleAuto->GetValue();
439
440 if( automatic && m_canvasScaleCtrl )
441 {
442 // set the scale to the auto value, without consulting the config
443 DPI_SCALING dpi( nullptr, this );
444
445 // update the field (no events sent)
446 m_canvasScaleCtrl->SetValue( dpi.GetScaleFactor() );
447 }
448}
449
450
451void PANEL_COMMON_SETTINGS::OnTextEditorClick( wxCommandEvent& event )
452{
453 // Ask the user to select a new editor, but suggest the current one as the default.
454 wxString editorname = Pgm().AskUserForPreferredEditor( m_textEditorPath->GetValue() );
455
456 // If we have a new editor name request it to be copied to m_text_editor and saved
457 // to the preferences file. If the user cancelled the dialog then the previous
458 // value will be retained.
459 if( !editorname.IsEmpty() )
460 m_textEditorPath->SetValue( editorname );
461}
462
463
464void PANEL_COMMON_SETTINGS::OnPDFViewerClick( wxCommandEvent& event )
465{
466 wxString mask( wxT( "*" ) );
467
468#ifdef __WINDOWS__
469 mask += wxT( ".exe" );
470#endif
471
472 wxString wildcard = _( "Executable files (" ) + mask + wxT( ")|" ) + mask;
473
474 Pgm().ReadPdfBrowserInfos();
475 wxFileName fn = Pgm().GetPdfBrowserName();
476
477 wxFileDialog dlg( this, _( "Select Preferred PDF Viewer" ), fn.GetPath(), fn.GetFullPath(),
478 wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST );
479
480 if( dlg.ShowModal() == wxID_CANCEL )
481 return;
482
483 m_otherPDFViewer->SetValue( true );
484 m_PDFViewerPath->SetValue( dlg.GetPath() );
485}
486
487
489{
491}
492
493
495{
496 m_PDFViewerPath->Enable( m_otherPDFViewer->GetValue() );
497 m_pdfViewerBtn->Enable( m_otherPDFViewer->GetValue() );
498}
wxBitmap KiBitmap(BITMAPS aBitmap, int aHeightTag)
Construct a wxBitmap from an image identifier Returns the image from the active theme if the image ha...
Definition: bitmap.cpp:105
int KiIconScale(wxWindow *aWindow)
Return the automatic scale factor that would be used for a given window by KiScaledBitmap and KiScale...
Definition: bitmap.cpp:122
@ small_folder
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
APPEARANCE m_Appearance
AUTO_BACKUP m_Backup
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition: dialog_shim.h:83
Class to handle configuration and automatic determination of the DPI scale to use for canvases.
Definition: dpi_scaling.h:37
double GetScaleFactor() const
Get the DPI scale from all known sources in order:
void SetDpiConfig(bool aAuto, double aValue)
Set the common DPI config in a given config object.
static double GetDefaultScaleFactor()
Get the "default" scaling factor to use if not other config is available.
static double GetMaxScaleFactor()
bool GetCanvasIsAutoScaled() const
Is the current value auto scaled, or is it user-set in the config.
static double GetMinScaleFactor()
void ResetToDefaults()
Resets all parameters to default values.
Class PANEL_COMMON_SETTINGS_BASE.
STEPPED_SLIDER * m_iconScaleSlider
void OnTextEditorClick(wxCommandEvent &event) override
bool TransferDataFromWindow() override
bool TransferDataToWindow() override
void OnCanvasScaleChange(wxCommandEvent &aEvent)
Event fired when the canvas scale field is modified.
void applySettingsToPanel(COMMON_SETTINGS &aSettings)
void OnScaleSlider(wxScrollEvent &aEvent)
PANEL_COMMON_SETTINGS(DIALOG_SHIM *aDialog, wxWindow *aParent)
int m_last_scale
saved icon scale when Auto selected
void OnPDFViewerClick(wxCommandEvent &event) override
void OnIconScaleAuto(wxCommandEvent &aEvent)
void OnCanvasScaleAuto(wxCommandEvent &aEvent) override
Event fired when the canvas auto-scale option is changed.
void ResetPanel() override
Reset the contents of this panel.
void OnRadioButtonPdfViewer(wxCommandEvent &event) override
Customized wxSlider with forced stepping.
void SetStep(int aSize)
Set the step size.
#define _(s)
bool AllowIconsInMenus()
If the user has disabled icons system-wide, we check that here.
Definition: gtk/ui.cpp:140
wxFont GetInfoFont(wxWindow *aWindow)
Definition: ui_common.cpp:144
see class PGM_BASE
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, const CPTREE &aTree)
Output a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:200
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
Definition: single_top.cpp:111
int min_interval
Minimum time, in seconds, between subsequent backups.
bool backup_on_autosave
Trigger a backup on autosave.
unsigned long long limit_total_size
Maximum total size of backups (bytes), 0 for unlimited.
int limit_total_files
Maximum number of backup archives to retain.
int limit_daily_files
Maximum files to keep per day, 0 for unlimited.
bool enabled
Automatically back up the project when files are saved.