diff --git a/main.cc b/main.cc
index 4ac0054..376c075 100644
--- a/main.cc
+++ b/main.cc
@@ -209,12 +209,31 @@ void T_Main::makeUI( )
 	if ( BeginMainMenuBar( ) ) {
 		if ( BeginMenu( "File" ) ) {
 			if ( MenuItem( "Save curves" , "C-s" , false , sync.curvesModified( ) ) ) {
-				// FIXME: confirmation dialog if file changed
-				sync.saveCurves( );
+				if ( sync.curvesFileChanged( ) ) {
+					Globals::Window( ).msgbox(
+							"Curves file changed" ,
+							"The file containing the curves has been modified "
+							"on the disk. These changes will be overwritten. "
+							"Do you want to continue?" ,
+							[]( auto b ) {
+								if ( b == T_MessageBox::BT_YES ) {
+									Globals::Sync( ).saveCurves( );
+								}
+							} , { T_MessageBox::BT_YES , T_MessageBox::BT_NO } );
+				} else {
+					sync.saveCurves( );
+				}
 			}
 			if ( MenuItem( "Reload curves" , "C-R" , false , sync.curvesModified( ) ) ) {
-				// FIXME: confirmation dialog
-				sync.loadCurves( );
+				Globals::Window( ).msgbox(
+						"Reload curves?" ,
+						"Changes you made to the curves will be lost. Do you "
+						"want to continue?" ,
+						[]( auto b ) {
+							if ( b == T_MessageBox::BT_YES ) {
+								Globals::Sync( ).loadCurves( );
+							}
+						} , { T_MessageBox::BT_YES , T_MessageBox::BT_NO } );
 			}
 			Separator( );
 			if ( MenuItem( "Undo" , "C-z" , false , undo.canUndo( ) ) ) {
diff --git a/syncview.cc b/syncview.cc
index 5547bc8..481fe03 100644
--- a/syncview.cc
+++ b/syncview.cc
@@ -70,8 +70,8 @@ class T_ChangeDurationDialog_ : public A_ModalDialog
 	bool scale_{ true };
 
     protected:
-	bool drawDialog( ) noexcept override;
-	void onOK( ) noexcept override;
+	uint8_t drawDialog( ) noexcept override;
+	bool onButton( uint8_t id ) noexcept override;
 
     public:
 	T_ChangeDurationDialog_(
@@ -90,9 +90,11 @@ T_ChangeDurationDialog_::T_ChangeDurationDialog_(
 		units_{ units0_ } , uSize_{ uSize0_ } , uPerMinute_{ uPerMinute0_ }
 {
 	setInitialSize( 300.f , 180.f );
+	addButton( "OK" );
+	addButton( "Cancel" );
 }
 
-bool T_ChangeDurationDialog_::drawDialog( ) noexcept
+uint8_t T_ChangeDurationDialog_::drawDialog( ) noexcept
 {
 	using namespace ImGui;
 
@@ -137,14 +139,19 @@ bool T_ChangeDurationDialog_::drawDialog( ) noexcept
 		PopStyleVar( );
 	}
 
-	return units_ != units0_ || uPerMinute_ != uPerMinute0_;
+	const bool eo{ units_ != units0_ || uPerMinute_ != uPerMinute0_ };
+	return eo ? 3 : 2;
 }
 
-void T_ChangeDurationDialog_::onOK( ) noexcept
+bool T_ChangeDurationDialog_::onButton(
+		const uint8_t button ) noexcept
 {
-	SyncEditor::SetDuration( units_ ,
-			uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ ,
-			scale_ );
+	if ( button == 0 ) {
+		SyncEditor::SetDuration( units_ ,
+				uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ ,
+				scale_ );
+	}
+	return true;
 }
 
 
diff --git a/window.cc b/window.cc
index 572138f..89611d8 100644
--- a/window.cc
+++ b/window.cc
@@ -11,20 +11,46 @@ A_ModalDialog::A_ModalDialog(
 	: id_{ id , uint32_t( strlen( id ) + 1 ) }
 {}
 
+A_ModalDialog::A_ModalDialog(
+		T_String const& id ) noexcept
+{
+	if ( id && id[ id.length( ) - 1 ] == 0 ) {
+		id_ = ebcl::T_Buffer< char >{ id.data( ) , id.size( ) };
+	} else {
+		id_ = id.toOSString( );
+	}
+}
+
 A_ModalDialog::~A_ModalDialog( )
 { }
 
+uint8_t A_ModalDialog::addButton(
+		char const* name ) noexcept
+{
+	buttons_.addNew( name , strlen( name ) + 1 );
+	return buttons_.size( ) - 1;
+}
+
+uint8_t A_ModalDialog::addButton(
+		T_String const& name ) noexcept
+{
+	if ( name && name[ name.length( ) - 1 ] == 0 ) {
+		buttons_.addNew( name.data( ) , name.size( ) );
+	} else {
+		buttons_.add( name.toOSString( ) );
+	}
+	return buttons_.size( );
+}
+
 void A_ModalDialog::initDialog( ) noexcept
 {}
 
-void A_ModalDialog::onCancel( ) noexcept
-{ }
-
 bool A_ModalDialog::draw( ) noexcept
 {
 	using namespace ImGui;
 	if ( !open_ ) {
-		OpenPopup( id_.data( ) );
+		assert( buttons_.size( ) );
+		OpenPopup( &id_[ 0 ] );
 		open_ = true;
 	}
 
@@ -33,49 +59,115 @@ bool A_ModalDialog::draw( ) noexcept
 		SetNextWindowSize( *initialSize_ , ImGuiCond_Appearing );
 	}
 
-	bool ok{ false };
-	bool keep{ BeginPopupModal( id_.data( ) , nullptr , ImGuiWindowFlags_NoResize ) };
-	if ( keep ) {
-		const bool canUseOK{ drawDialog( ) };
+	BeginPopupModal( &id_[ 0 ] , nullptr , ImGuiWindowFlags_NoResize );
+	const auto btMask{ drawDialog( ) };
+	assert( btMask != 0 );
 
-		const ImVec2 ws( GetWindowContentRegionMax( ) );
-		auto const& style( GetStyle( ) );
-		const float innerWidth{ ws.x - 2 * style.FramePadding.x };
-		constexpr float nButtons{ 2.f };
-		const ImVec2 buttonSize{ std::max( 40.f ,
-				( innerWidth - nButtons * style.FramePadding.x ) / nButtons ) ,
-		      0.f
-		};
+	const ImVec2 ws( GetWindowContentRegionMax( ) );
+	auto const& style( GetStyle( ) );
+	const float innerWidth{ ws.x - 2 * style.FramePadding.x };
+	const auto nButtons{ buttons_.size( ) };
+	const ImVec2 buttonSize{
+		std::max( 40.f , ( innerWidth - nButtons * style.FramePadding.x ) / nButtons ) ,
+		0.f
+	};
 
-		if ( !canUseOK ) {
+	int32_t clicked{ -1 };
+	for ( auto i = 0u ; i < buttons_.size( ) ; i ++ ) {
+		const auto m{ 1 << i };
+		const bool d{ ( btMask & m ) == 0 };
+		if ( i ) {
+			SameLine( 0 );
+		}
+		if ( d ) {
 			PushItemFlag( ImGuiItemFlags_Disabled , true );
 			PushStyleVar( ImGuiStyleVar_Alpha ,
 					GetStyle( ).Alpha * .5f );
 		}
-		if ( Button( "OK" , buttonSize ) ) {
-			ok = true;
-			keep = false;
+		if ( Button( &buttons_[ i ][ 0 ] , buttonSize ) ) {
+			assert( clicked == -1 );
+			clicked = i;
 		}
-		if ( !canUseOK ) {
+		if ( d ) {
 			PopItemFlag( );
 			PopStyleVar( );
 		}
-		SameLine( 0 );
-		keep = !Button( "Cancel" , buttonSize ) && keep;
+	}
+	const bool close( clicked != -1 && onButton( clicked ) );
+	if ( close ) {
+		CloseCurrentPopup( );
+	}
+	EndPopup( );
+	return !close;
+}
 
-		if ( !keep ) {
-			CloseCurrentPopup( );
+
+/*= T_MessageBox =============================================================*/
+
+void T_MessageBox::setButtons(
+		const T_Buttons buttons ) noexcept
+{
+	assert( buttons );
+	for ( auto i = 0u , nb = 0u ; i < 4 ; i ++ ) {
+		const E_Button b{ (E_Button) i };
+		const bool hasButton{ buttons & T_Buttons{ b } };
+		if ( !hasButton ) {
+			continue;
 		}
-		EndPopup( );
-	}
-	if ( !keep ) {
-		if ( ok ) {
-			onOK( );
-		} else {
-			onCancel( );
+		buttonMask_ |= ( 1 << nb );
+		nb ++;
+		buttons_.add( b );
+		switch ( b ) {
+		    case BT_OK: addButton( "OK" ); break;
+		    case BT_CANCEL: addButton( "Cancel" ); break;
+		    case BT_YES: addButton( "Yes" ); break;
+		    case BT_NO: addButton( "No" ); break;
 		}
 	}
-	return keep;
+}
+
+T_MessageBox::T_MessageBox(
+		char const* const title ,
+		char const* const text ,
+		F_Handler handler ,
+		const T_Buttons buttons ) noexcept
+	: A_ModalDialog( title ) , text_{ text , strlen( text ) + 1 } ,
+		handler_{ std::move( handler ) }
+{
+	setButtons( buttons );
+	setInitialSize( 300.f , 100.f );
+}
+
+T_MessageBox::T_MessageBox(
+		T_String const& title ,
+		T_String const& text ,
+		F_Handler handler ,
+		const T_Buttons buttons ) noexcept
+	: A_ModalDialog( title ) , handler_{ std::move( handler ) }
+{
+	if ( text && text[ text.length( ) - 1 ] == 0 ) {
+		text_ = ebcl::T_Buffer< char >{ text.data( ) , text.size( ) };
+	} else {
+		text_ = text.toOSString( );
+	}
+	setButtons( buttons );
+	setInitialSize( 300.f , 100.f );
+}
+
+uint8_t T_MessageBox::drawDialog( ) noexcept
+{
+	ImGui::TextWrapped( "%s" , &text_[ 0 ] );
+	return buttonMask_;
+}
+
+bool T_MessageBox::onButton(
+		uint8_t button ) noexcept
+{
+	assert( button < buttons_.size( ) );
+	if ( handler_ ) {
+		handler_( buttons_[ button ] );
+	}
+	return true;
 }
 
 
diff --git a/window.hh b/window.hh
index 9007e91..7d8b2ce 100644
--- a/window.hh
+++ b/window.hh
@@ -9,29 +9,34 @@
 class A_ModalDialog
 {
     private:
-	const T_String id_;
+	ebcl::T_Buffer< char > id_;
 	bool open_{ false };
 	T_Optional< ImVec2 > initialSize_;
+	T_StaticArray< ebcl::T_Buffer< char > , 4 > buttons_;
 
     protected:
 	A_ModalDialog( char const* id ) noexcept;
+	A_ModalDialog( T_String const& id ) noexcept;
 
 	void setInitialSize( const float width ,
 			const float height ) noexcept
 		{ initialSize_.setNew( width , height ); }
 
+	// Add a button w/ the specified name, return its identifier
+	uint8_t addButton( char const* name ) noexcept;
+	uint8_t addButton( T_String const& name ) noexcept;
+
 	// Initialisation, called right before the window is opened.
 	// Does nothing by default.
 	virtual void initDialog( ) noexcept;
 
-	// Draw the dialog box's contents, returns true if the OK button
-	// should be enabled.
-	virtual bool drawDialog( ) noexcept = 0;
+	// Draw the dialog box's contents, returns a mask that determines
+	// which buttons should be enabled.
+	virtual uint8_t drawDialog( ) noexcept = 0;
 
-	// Handlers for both OK and Cancel. The former must be overridden,
-	// the latter defaults to "do nothing".
-	virtual void onOK( ) noexcept = 0;
-	virtual void onCancel( ) noexcept;
+	// Button click handler. Returns true if the dialog should be
+	// closed.
+	virtual bool onButton( uint8_t button ) noexcept = 0;
 
     public:
 	NO_COPY( A_ModalDialog );
@@ -45,6 +50,44 @@ class A_ModalDialog
 using P_ModalDialog = T_OwnPtr< A_ModalDialog >;
 
 
+class T_MessageBox : public A_ModalDialog
+{
+    public:
+	enum E_Button {
+		BT_OK ,
+		BT_CANCEL ,
+		BT_YES ,
+		BT_NO ,
+	};
+	using T_Buttons = T_Flags< E_Button >;
+
+	// Result handler
+	using F_Handler = std::function< void( E_Button ) >;
+
+    private:
+	ebcl::T_Buffer< char > text_;
+	F_Handler handler_;
+	uint8_t buttonMask_{ 0 };
+	T_StaticArray< E_Button , 4 > buttons_;
+
+	void setButtons( T_Buttons buttons ) noexcept;
+
+    protected:
+	uint8_t drawDialog( ) noexcept override;
+	bool onButton( uint8_t button ) noexcept override;
+
+    public:
+	T_MessageBox( char const* title ,
+			char const* text ,
+			F_Handler handler = { } ,
+			T_Buttons buttons = BT_OK ) noexcept;
+	T_MessageBox( T_String const& title ,
+			T_String const& text ,
+			F_Handler handler = { } ,
+			T_Buttons buttons = BT_OK ) noexcept;
+};
+
+
 struct T_Window
 {
 	T_Window( );
@@ -58,6 +101,18 @@ struct T_Window
 
 	void pushDialog( P_ModalDialog dialog ) noexcept
 		{ modals_.add( std::move( dialog ) ); }
+
+	void msgbox( char const* title ,
+			char const* text ,
+			T_MessageBox::F_Handler handler = { } ,
+			T_MessageBox::T_Buttons buttons = T_MessageBox::BT_OK )
+		{ modals_.add( NewOwned< T_MessageBox >( title , text , handler , buttons ) ); }
+	void msgbox( T_String const& title ,
+			T_String const& text ,
+			T_MessageBox::F_Handler handler = { } ,
+			T_MessageBox::T_Buttons buttons = T_MessageBox::BT_OK )
+		{ modals_.add( NewOwned< T_MessageBox >( title , text , handler , buttons ) ); }
+
 	void handleDialogs( ) noexcept;
 
 	ImFont* defaultFont( ) const noexcept