2019-07-11 21:52:33 +03:00
# include "TextEditorWidget.h"
2019-07-14 03:58:04 +03:00
# include <AK/Optional.h>
2019-07-11 21:52:33 +03:00
# include <AK/StringBuilder.h>
2019-12-19 22:20:20 +03:00
# include <AK/URL.h>
2019-07-11 21:52:33 +03:00
# include <LibCore/CFile.h>
2019-09-02 20:48:47 +03:00
# include <LibDraw/PNGLoader.h>
# include <LibGUI/GAboutDialog.h>
2019-07-11 21:52:33 +03:00
# include <LibGUI/GAction.h>
# include <LibGUI/GBoxLayout.h>
2019-08-21 22:30:20 +03:00
# include <LibGUI/GButton.h>
2019-07-11 21:52:33 +03:00
# include <LibGUI/GFilePicker.h>
# include <LibGUI/GFontDatabase.h>
# include <LibGUI/GMenuBar.h>
# include <LibGUI/GMessageBox.h>
# include <LibGUI/GStatusBar.h>
2019-08-21 22:30:20 +03:00
# include <LibGUI/GTextBox.h>
2019-07-11 21:52:33 +03:00
# include <LibGUI/GTextEditor.h>
# include <LibGUI/GToolBar.h>
TextEditorWidget : : TextEditorWidget ( )
{
set_layout ( make < GBoxLayout > ( Orientation : : Vertical ) ) ;
layout ( ) - > set_spacing ( 0 ) ;
2019-09-21 17:27:54 +03:00
auto toolbar = GToolBar : : construct ( this ) ;
2019-09-21 16:43:52 +03:00
m_editor = GTextEditor : : construct ( GTextEditor : : MultiLine , this ) ;
2019-07-11 21:52:33 +03:00
m_editor - > set_ruler_visible ( true ) ;
m_editor - > set_automatic_indentation_enabled ( true ) ;
2019-08-27 18:05:01 +03:00
m_editor - > set_line_wrapping_enabled ( true ) ;
2019-08-21 22:30:20 +03:00
2019-08-27 21:18:19 +03:00
m_editor - > on_change = [ this ] {
2019-12-05 09:57:54 +03:00
// Do not mark as diry on the first change (When document is first opened.)
if ( m_document_opening ) {
m_document_opening = false ;
return ;
}
2019-08-27 21:18:19 +03:00
bool was_dirty = m_document_dirty ;
m_document_dirty = true ;
if ( ! was_dirty )
update_title ( ) ;
} ;
2019-09-21 18:05:35 +03:00
m_find_widget = GWidget : : construct ( this ) ;
2019-08-22 12:09:25 +03:00
m_find_widget - > set_fill_with_background_color ( true ) ;
m_find_widget - > set_size_policy ( SizePolicy : : Fill , SizePolicy : : Fixed ) ;
m_find_widget - > set_preferred_size ( 0 , 22 ) ;
m_find_widget - > set_layout ( make < GBoxLayout > ( Orientation : : Horizontal ) ) ;
m_find_widget - > layout ( ) - > set_margins ( { 2 , 2 , 2 , 2 } ) ;
m_find_widget - > set_visible ( false ) ;
2019-08-21 22:30:20 +03:00
2019-09-21 16:43:52 +03:00
m_find_textbox = GTextBox : : construct ( m_find_widget ) ;
2019-08-21 22:30:20 +03:00
2019-08-25 22:35:58 +03:00
m_find_next_action = GAction : : create ( " Find next " , { Mod_Ctrl , Key_G } , [ & ] ( auto & ) {
2019-08-21 22:30:20 +03:00
auto needle = m_find_textbox - > text ( ) ;
2019-10-15 09:05:45 +03:00
if ( needle . is_empty ( ) ) {
dbg ( ) < < " find_next( \" \" ) " ;
return ;
}
2019-11-01 21:19:39 +03:00
auto found_range = m_editor - > document ( ) . find_next ( needle , m_editor - > normalized_selection ( ) . end ( ) ) ;
2019-08-24 21:09:35 +03:00
dbg ( ) < < " find_next( \" " < < needle < < " \" ) returned " < < found_range ;
if ( found_range . is_valid ( ) ) {
m_editor - > set_selection ( found_range ) ;
} else {
GMessageBox : : show (
String : : format ( " Not found: \" %s \" " , needle . characters ( ) ) ,
" Not found " ,
GMessageBox : : Type : : Information ,
GMessageBox : : InputType : : OK , window ( ) ) ;
}
2019-08-25 22:35:58 +03:00
} ) ;
m_find_previous_action = GAction : : create ( " Find previous " , { Mod_Ctrl | Mod_Shift , Key_G } , [ & ] ( auto & ) {
2019-08-24 21:09:35 +03:00
auto needle = m_find_textbox - > text ( ) ;
2019-10-15 09:05:45 +03:00
if ( needle . is_empty ( ) ) {
dbg ( ) < < " find_prev( \" \" ) " ;
return ;
}
2019-08-24 21:09:35 +03:00
auto selection_start = m_editor - > normalized_selection ( ) . start ( ) ;
if ( ! selection_start . is_valid ( ) )
selection_start = m_editor - > normalized_selection ( ) . end ( ) ;
2019-11-01 21:19:39 +03:00
auto found_range = m_editor - > document ( ) . find_previous ( needle , selection_start ) ;
2019-08-25 13:23:34 +03:00
2019-08-24 21:09:35 +03:00
dbg ( ) < < " find_prev( \" " < < needle < < " \" ) returned " < < found_range ;
2019-08-21 22:30:20 +03:00
if ( found_range . is_valid ( ) ) {
m_editor - > set_selection ( found_range ) ;
} else {
GMessageBox : : show (
String : : format ( " Not found: \" %s \" " , needle . characters ( ) ) ,
" Not found " ,
GMessageBox : : Type : : Information ,
GMessageBox : : InputType : : OK , window ( ) ) ;
}
2019-08-25 22:35:58 +03:00
} ) ;
2019-09-21 20:28:28 +03:00
m_find_previous_button = GButton : : construct ( " Previous " , m_find_widget ) ;
2019-08-25 22:35:58 +03:00
m_find_previous_button - > set_size_policy ( SizePolicy : : Fixed , SizePolicy : : Fill ) ;
m_find_previous_button - > set_preferred_size ( 64 , 0 ) ;
m_find_previous_button - > set_action ( * m_find_previous_action ) ;
2019-09-21 20:28:28 +03:00
m_find_next_button = GButton : : construct ( " Next " , m_find_widget ) ;
2019-08-25 22:35:58 +03:00
m_find_next_button - > set_size_policy ( SizePolicy : : Fixed , SizePolicy : : Fill ) ;
m_find_next_button - > set_preferred_size ( 64 , 0 ) ;
m_find_next_button - > set_action ( * m_find_next_action ) ;
2019-08-21 22:30:20 +03:00
2019-08-22 12:09:25 +03:00
m_find_textbox - > on_return_pressed = [ this ] {
2019-08-24 21:09:35 +03:00
m_find_next_button - > click ( ) ;
2019-08-22 12:09:25 +03:00
} ;
m_find_textbox - > on_escape_pressed = [ this ] {
m_find_widget - > set_visible ( false ) ;
m_editor - > set_focus ( true ) ;
} ;
2019-09-14 00:56:37 +03:00
m_find_action = GAction : : create ( " Find... " , { Mod_Ctrl , Key_F } , load_png ( " /res/icons/16x16/find.png " ) , [ this ] ( auto & ) {
2019-08-22 12:09:25 +03:00
m_find_widget - > set_visible ( true ) ;
2019-08-22 12:02:03 +03:00
m_find_textbox - > set_focus ( true ) ;
2019-08-25 22:44:59 +03:00
m_find_textbox - > select_all ( ) ;
2019-08-22 12:02:03 +03:00
} ) ;
2019-08-25 22:38:13 +03:00
m_editor - > add_custom_context_menu_action ( * m_find_action ) ;
m_editor - > add_custom_context_menu_action ( * m_find_next_action ) ;
m_editor - > add_custom_context_menu_action ( * m_find_previous_action ) ;
2019-09-21 17:29:47 +03:00
m_statusbar = GStatusBar : : construct ( this ) ;
2019-07-11 21:52:33 +03:00
2019-09-21 17:29:47 +03:00
m_editor - > on_cursor_change = [ this ] {
2019-07-11 21:52:33 +03:00
StringBuilder builder ;
2019-07-27 22:20:38 +03:00
builder . appendf ( " Line: %d, Column: %d " , m_editor - > cursor ( ) . line ( ) + 1 , m_editor - > cursor ( ) . column ( ) ) ;
2019-09-21 17:29:47 +03:00
m_statusbar - > set_text ( builder . to_string ( ) ) ;
2019-07-11 21:52:33 +03:00
} ;
2019-09-06 08:05:28 +03:00
m_new_action = GAction : : create ( " New " , { Mod_Ctrl , Key_N } , GraphicsBitmap : : load_from_file ( " /res/icons/16x16/new.png " ) , [ this ] ( const GAction & ) {
if ( m_document_dirty ) {
2019-09-30 18:20:53 +03:00
auto save_document_first_box = GMessageBox : : construct ( " Save Document First? " , " Warning " , GMessageBox : : Type : : Warning , GMessageBox : : InputType : : OKCancel , window ( ) ) ;
auto save_document_first_result = save_document_first_box - > exec ( ) ;
2019-09-06 08:05:28 +03:00
if ( save_document_first_result ! = GDialog : : ExecResult : : ExecOK )
2019-09-14 23:10:44 +03:00
return ;
2019-09-06 08:05:28 +03:00
m_save_action - > activate ( ) ;
}
2019-09-14 23:10:44 +03:00
2019-09-06 08:05:28 +03:00
m_document_dirty = false ;
m_editor - > set_text ( StringView ( ) ) ;
set_path ( FileSystemPath ( ) ) ;
update_title ( ) ;
2019-07-11 21:52:33 +03:00
} ) ;
2019-09-20 20:36:39 +03:00
m_open_action = GCommonActions : : make_open_action ( [ this ] ( auto & ) {
2019-07-29 07:45:50 +03:00
Optional < String > open_path = GFilePicker : : get_open_filepath ( ) ;
2019-07-11 21:52:33 +03:00
2019-07-29 07:45:50 +03:00
if ( ! open_path . has_value ( ) )
2019-07-14 03:58:04 +03:00
return ;
2019-12-22 01:20:43 +03:00
if ( m_document_dirty ) {
auto save_document_first_box = GMessageBox : : construct ( " Save Document First? " , " Warning " , GMessageBox : : Type : : Warning , GMessageBox : : InputType : : OKCancel , window ( ) ) ;
auto save_document_first_result = save_document_first_box - > exec ( ) ;
if ( save_document_first_result = = GDialog : : ExecResult : : ExecOK )
m_save_action - > activate ( ) ;
}
2019-07-29 07:45:50 +03:00
open_sesame ( open_path . value ( ) ) ;
2019-07-11 21:52:33 +03:00
} ) ;
2019-07-26 07:48:39 +03:00
m_save_as_action = GAction : : create ( " Save as... " , { Mod_None , Key_F12 } , GraphicsBitmap : : load_from_file ( " /res/icons/16x16/save.png " ) , [ this ] ( const GAction & ) {
2019-07-29 07:45:50 +03:00
Optional < String > save_path = GFilePicker : : get_save_filepath ( m_name . is_null ( ) ? " Untitled " : m_name , m_extension . is_null ( ) ? " txt " : m_extension ) ;
if ( ! save_path . has_value ( ) )
2019-07-14 03:58:04 +03:00
return ;
2019-07-29 07:45:50 +03:00
if ( ! m_editor - > write_to_file ( save_path . value ( ) ) ) {
2019-07-16 22:32:10 +03:00
GMessageBox : : show ( " Unable to save file. \n " , " Error " , GMessageBox : : Type : : Error , GMessageBox : : InputType : : OK , window ( ) ) ;
2019-07-14 03:58:04 +03:00
return ;
}
2019-08-27 21:18:19 +03:00
m_document_dirty = false ;
2019-07-29 07:45:50 +03:00
set_path ( FileSystemPath ( save_path . value ( ) ) ) ;
dbg ( ) < < " Wrote document to " < < save_path . value ( ) ;
2019-07-24 07:32:30 +03:00
} ) ;
2019-07-26 07:48:39 +03:00
m_save_action = GAction : : create ( " Save " , { Mod_Ctrl , Key_S } , GraphicsBitmap : : load_from_file ( " /res/icons/16x16/save.png " ) , [ & ] ( const GAction & ) {
2019-07-24 07:32:30 +03:00
if ( ! m_path . is_empty ( ) ) {
2019-08-27 21:18:19 +03:00
if ( ! m_editor - > write_to_file ( m_path ) ) {
2019-07-24 07:32:30 +03:00
GMessageBox : : show ( " Unable to save file. \n " , " Error " , GMessageBox : : Type : : Error , GMessageBox : : InputType : : OK , window ( ) ) ;
2019-08-27 21:18:19 +03:00
} else {
m_document_dirty = false ;
update_title ( ) ;
}
2019-07-24 07:32:30 +03:00
return ;
}
2019-07-26 07:48:39 +03:00
m_save_as_action - > activate ( ) ;
2019-07-11 21:52:33 +03:00
} ) ;
2019-08-25 13:23:34 +03:00
m_line_wrapping_setting_action = GAction : : create ( " Line wrapping " , [ & ] ( GAction & action ) {
action . set_checked ( ! action . is_checked ( ) ) ;
m_editor - > set_line_wrapping_enabled ( action . is_checked ( ) ) ;
} ) ;
m_line_wrapping_setting_action - > set_checkable ( true ) ;
m_line_wrapping_setting_action - > set_checked ( m_editor - > is_line_wrapping_enabled ( ) ) ;
2019-07-11 21:52:33 +03:00
auto menubar = make < GMenuBar > ( ) ;
2019-12-09 23:05:28 +03:00
auto app_menu = GMenu : : construct ( " Text Editor " ) ;
2019-08-26 18:59:05 +03:00
app_menu - > add_action ( * m_new_action ) ;
app_menu - > add_action ( * m_open_action ) ;
app_menu - > add_action ( * m_save_action ) ;
app_menu - > add_action ( * m_save_as_action ) ;
app_menu - > add_separator ( ) ;
2019-09-14 23:10:44 +03:00
app_menu - > add_action ( GCommonActions : : make_quit_action ( [ this ] ( auto & ) {
2019-08-27 21:37:41 +03:00
if ( ! request_close ( ) )
return ;
2019-07-11 21:52:33 +03:00
GApplication : : the ( ) . quit ( 0 ) ;
} ) ) ;
menubar - > add_menu ( move ( app_menu ) ) ;
2019-12-09 23:05:28 +03:00
auto edit_menu = GMenu : : construct ( " Edit " ) ;
2019-07-11 21:52:33 +03:00
edit_menu - > add_action ( m_editor - > undo_action ( ) ) ;
edit_menu - > add_action ( m_editor - > redo_action ( ) ) ;
edit_menu - > add_separator ( ) ;
edit_menu - > add_action ( m_editor - > cut_action ( ) ) ;
edit_menu - > add_action ( m_editor - > copy_action ( ) ) ;
edit_menu - > add_action ( m_editor - > paste_action ( ) ) ;
edit_menu - > add_action ( m_editor - > delete_action ( ) ) ;
2019-08-22 12:02:03 +03:00
edit_menu - > add_separator ( ) ;
edit_menu - > add_action ( * m_find_action ) ;
2019-08-25 22:35:58 +03:00
edit_menu - > add_action ( * m_find_next_action ) ;
edit_menu - > add_action ( * m_find_previous_action ) ;
2019-07-11 21:52:33 +03:00
menubar - > add_menu ( move ( edit_menu ) ) ;
2019-12-09 23:05:28 +03:00
auto font_menu = GMenu : : construct ( " Font " ) ;
2019-07-11 21:52:33 +03:00
GFontDatabase : : the ( ) . for_each_fixed_width_font ( [ & ] ( const StringView & font_name ) {
font_menu - > add_action ( GAction : : create ( font_name , [ this ] ( const GAction & action ) {
m_editor - > set_font ( GFontDatabase : : the ( ) . get_by_name ( action . text ( ) ) ) ;
m_editor - > update ( ) ;
} ) ) ;
} ) ;
2019-12-09 23:05:28 +03:00
auto view_menu = GMenu : : construct ( " View " ) ;
2019-08-27 18:01:52 +03:00
view_menu - > add_action ( * m_line_wrapping_setting_action ) ;
2019-08-28 22:13:30 +03:00
view_menu - > add_separator ( ) ;
view_menu - > add_submenu ( move ( font_menu ) ) ;
2019-08-27 18:01:52 +03:00
menubar - > add_menu ( move ( view_menu ) ) ;
2019-12-09 23:05:28 +03:00
auto help_menu = GMenu : : construct ( " Help " ) ;
2019-09-02 20:48:47 +03:00
help_menu - > add_action ( GAction : : create ( " About " , [ & ] ( const GAction & ) {
GAboutDialog : : show ( " TextEditor " , load_png ( " /res/icons/32x32/app-texteditor.png " ) , window ( ) ) ;
2019-07-11 21:52:33 +03:00
} ) ) ;
menubar - > add_menu ( move ( help_menu ) ) ;
GApplication : : the ( ) . set_menubar ( move ( menubar ) ) ;
2019-07-26 07:48:39 +03:00
toolbar - > add_action ( * m_new_action ) ;
toolbar - > add_action ( * m_open_action ) ;
toolbar - > add_action ( * m_save_action ) ;
2019-07-11 21:52:33 +03:00
toolbar - > add_separator ( ) ;
toolbar - > add_action ( m_editor - > cut_action ( ) ) ;
toolbar - > add_action ( m_editor - > copy_action ( ) ) ;
toolbar - > add_action ( m_editor - > paste_action ( ) ) ;
toolbar - > add_action ( m_editor - > delete_action ( ) ) ;
toolbar - > add_separator ( ) ;
toolbar - > add_action ( m_editor - > undo_action ( ) ) ;
toolbar - > add_action ( m_editor - > redo_action ( ) ) ;
}
TextEditorWidget : : ~ TextEditorWidget ( )
{
}
2019-07-29 07:45:50 +03:00
void TextEditorWidget : : set_path ( const FileSystemPath & file )
2019-07-24 07:32:30 +03:00
{
2019-07-29 07:45:50 +03:00
m_path = file . string ( ) ;
m_name = file . title ( ) ;
m_extension = file . extension ( ) ;
2019-08-27 21:18:19 +03:00
update_title ( ) ;
}
void TextEditorWidget : : update_title ( )
{
2019-07-24 07:32:30 +03:00
StringBuilder builder ;
builder . append ( " Text Editor: " ) ;
2019-08-27 21:18:19 +03:00
builder . append ( m_path ) ;
if ( m_document_dirty )
builder . append ( " (*) " ) ;
2019-07-24 07:32:30 +03:00
window ( ) - > set_title ( builder . to_string ( ) ) ;
}
2019-07-11 21:52:33 +03:00
void TextEditorWidget : : open_sesame ( const String & path )
{
2019-09-21 21:50:06 +03:00
auto file = CFile : : construct ( path ) ;
if ( ! file - > open ( CIODevice : : ReadOnly ) ) {
2019-07-16 22:32:10 +03:00
GMessageBox : : show ( String : : format ( " Opening \" %s \" failed: %s " , path . characters ( ) , strerror ( errno ) ) , " Error " , GMessageBox : : Type : : Error , GMessageBox : : InputType : : OK , window ( ) ) ;
2019-08-23 20:10:14 +03:00
return ;
2019-07-11 21:52:33 +03:00
}
2019-09-21 21:50:06 +03:00
m_editor - > set_text ( file - > read_all ( ) ) ;
2019-12-05 09:57:54 +03:00
m_document_dirty = false ;
m_document_opening = true ;
2019-07-29 07:45:50 +03:00
set_path ( FileSystemPath ( path ) ) ;
2019-07-16 22:32:10 +03:00
}
2019-08-27 21:37:41 +03:00
bool TextEditorWidget : : request_close ( )
{
if ( ! m_document_dirty )
return true ;
2019-09-21 21:32:31 +03:00
auto result = GMessageBox : : show ( " The document has been modified. Quit without saving? " , " Quit without saving? " , GMessageBox : : Type : : Warning , GMessageBox : : InputType : : OKCancel , window ( ) ) ;
2019-08-27 21:37:41 +03:00
return result = = GMessageBox : : ExecOK ;
}
2019-12-19 22:20:20 +03:00
void TextEditorWidget : : drop_event ( GDropEvent & event )
{
event . accept ( ) ;
window ( ) - > move_to_front ( ) ;
if ( event . data_type ( ) = = " url-list " ) {
auto lines = event . data ( ) . split_view ( ' \n ' ) ;
if ( lines . is_empty ( ) )
return ;
if ( lines . size ( ) > 1 ) {
GMessageBox : : show ( " TextEditor can only open one file at a time! " , " One at a time please! " , GMessageBox : : Type : : Error , GMessageBox : : InputType : : OK , window ( ) ) ;
return ;
}
URL url ( lines [ 0 ] ) ;
open_sesame ( url . path ( ) ) ;
}
}