2020-08-23 00:55:16 -03:00
/*
* Copyright ( c ) 2020 , the SerenityOS developers .
*
2021-04-22 05:24:48 -03:00
* SPDX - License - Identifier : BSD - 2 - Clause
2020-08-23 00:55:16 -03:00
*/
# include "SpreadsheetWidget.h"
2020-08-24 13:38:47 -03:00
# include "CellSyntaxHighlighter.h"
2020-08-24 12:35:20 -03:00
# include "HelpWindow.h"
2020-12-20 08:39:57 -03:00
# include "LibGUI/InputBox.h"
2020-08-23 00:55:16 -03:00
# include <LibGUI/BoxLayout.h>
2020-08-24 12:35:20 -03:00
# include <LibGUI/Button.h>
2020-12-22 02:15:15 -03:00
# include <LibGUI/FilePicker.h>
2020-08-24 12:21:25 -03:00
# include <LibGUI/Label.h>
2020-12-20 08:39:57 -03:00
# include <LibGUI/Menu.h>
2020-08-23 00:55:16 -03:00
# include <LibGUI/MessageBox.h>
2020-08-24 12:21:25 -03:00
# include <LibGUI/Splitter.h>
2020-08-23 00:55:16 -03:00
# include <LibGUI/TabWidget.h>
2020-08-24 12:21:25 -03:00
# include <LibGUI/TextEditor.h>
2020-12-29 14:25:13 -03:00
# include <LibGfx/FontDatabase.h>
2020-08-23 00:55:16 -03:00
# include <string.h>
namespace Spreadsheet {
2020-08-24 17:23:22 -03:00
SpreadsheetWidget : : SpreadsheetWidget ( NonnullRefPtrVector < Sheet > & & sheets , bool should_add_sheet_if_empty )
2020-08-25 22:30:23 -03:00
: m_workbook ( make < Workbook > ( move ( sheets ) ) )
2020-08-23 00:55:16 -03:00
{
set_fill_with_background_color ( true ) ;
Userland+LibGUI: Add shorthand versions of the Margins constructor
This allows for typing [8] instead of [8, 8, 8, 8] to specify the same
margin on all edges, for example. The constructors follow CSS' style of
specifying margins. The added constructors are:
- Margins(int all): Sets the same margin on all edges.
- Margins(int vertical, int horizontal): Sets the first argument to top
and bottom margins, and the second argument to left and right margins.
- Margins(int top, int vertical, int bottom): Sets the first argument to
the top margin, the second argument to the left and right margins,
and the third argument to the bottom margin.
2021-08-16 21:11:38 -03:00
set_layout < GUI : : VerticalBoxLayout > ( ) . set_margins ( 2 ) ;
2020-08-24 12:21:25 -03:00
auto & container = add < GUI : : VerticalSplitter > ( ) ;
auto & top_bar = container . add < GUI : : Frame > ( ) ;
top_bar . set_layout < GUI : : HorizontalBoxLayout > ( ) . set_spacing ( 1 ) ;
2020-12-29 21:23:32 -03:00
top_bar . set_fixed_height ( 26 ) ;
2020-08-24 12:21:25 -03:00
auto & current_cell_label = top_bar . add < GUI : : Label > ( " " ) ;
2020-12-29 21:23:32 -03:00
current_cell_label . set_fixed_width ( 50 ) ;
2020-08-24 12:35:20 -03:00
auto & help_button = top_bar . add < GUI : : Button > ( " 🛈 " ) ;
2020-12-29 21:23:32 -03:00
help_button . set_fixed_size ( 20 , 20 ) ;
2020-08-24 12:35:20 -03:00
help_button . on_click = [ & ] ( auto ) {
2021-03-22 10:02:21 -03:00
if ( ! m_selected_view ) {
GUI : : MessageBox : : show_error ( window ( ) , " Can only show function documentation/help when a worksheet exists and is open " ) ;
} else if ( auto * sheet_ptr = m_selected_view - > sheet_if_available ( ) ) {
auto docs = sheet_ptr - > gather_documentation ( ) ;
auto help_window = HelpWindow : : the ( window ( ) ) ;
help_window - > set_docs ( move ( docs ) ) ;
help_window - > show ( ) ;
}
2020-08-24 12:35:20 -03:00
} ;
2020-08-24 12:21:25 -03:00
auto & cell_value_editor = top_bar . add < GUI : : TextEditor > ( GUI : : TextEditor : : Type : : SingleLine ) ;
2020-12-29 14:25:13 -03:00
cell_value_editor . set_font ( Gfx : : FontDatabase : : default_fixed_width_font ( ) ) ;
2020-08-24 12:21:25 -03:00
cell_value_editor . set_scrollbars_enabled ( false ) ;
2021-07-03 11:05:53 -03:00
cell_value_editor . on_return_pressed = [ this ] ( ) {
m_selected_view - > move_cursor ( GUI : : AbstractView : : CursorMovement : : Down ) ;
} ;
2020-08-24 13:38:47 -03:00
cell_value_editor . set_syntax_highlighter ( make < CellSyntaxHighlighter > ( ) ) ;
2020-08-24 12:21:25 -03:00
cell_value_editor . set_enabled ( false ) ;
current_cell_label . set_enabled ( false ) ;
m_tab_widget = container . add < GUI : : TabWidget > ( ) ;
2020-08-23 00:55:16 -03:00
m_tab_widget - > set_tab_position ( GUI : : TabWidget : : TabPosition : : Bottom ) ;
2020-08-24 17:23:22 -03:00
m_cell_value_editor = cell_value_editor ;
m_current_cell_label = current_cell_label ;
2020-12-28 17:03:16 -03:00
m_inline_documentation_window = GUI : : Window : : construct ( window ( ) ) ;
m_inline_documentation_window - > set_rect ( m_cell_value_editor - > rect ( ) . translated ( 0 , m_cell_value_editor - > height ( ) + 7 ) . inflated ( 6 , 6 ) ) ;
m_inline_documentation_window - > set_window_type ( GUI : : WindowType : : Tooltip ) ;
m_inline_documentation_window - > set_resizable ( false ) ;
auto & inline_widget = m_inline_documentation_window - > set_main_widget < GUI : : Frame > ( ) ;
inline_widget . set_fill_with_background_color ( true ) ;
Userland+LibGUI: Add shorthand versions of the Margins constructor
This allows for typing [8] instead of [8, 8, 8, 8] to specify the same
margin on all edges, for example. The constructors follow CSS' style of
specifying margins. The added constructors are:
- Margins(int all): Sets the same margin on all edges.
- Margins(int vertical, int horizontal): Sets the first argument to top
and bottom margins, and the second argument to left and right margins.
- Margins(int top, int vertical, int bottom): Sets the first argument to
the top margin, the second argument to the left and right margins,
and the third argument to the bottom margin.
2021-08-16 21:11:38 -03:00
inline_widget . set_layout < GUI : : VerticalBoxLayout > ( ) . set_margins ( 4 ) ;
2020-12-28 17:03:16 -03:00
inline_widget . set_frame_shape ( Gfx : : FrameShape : : Box ) ;
m_inline_documentation_label = inline_widget . add < GUI : : Label > ( ) ;
m_inline_documentation_label - > set_fill_with_background_color ( true ) ;
m_inline_documentation_label - > set_autosize ( false ) ;
m_inline_documentation_label - > set_text_alignment ( Gfx : : TextAlignment : : CenterLeft ) ;
2020-08-24 17:23:22 -03:00
2020-08-25 22:30:23 -03:00
if ( ! m_workbook - > has_sheets ( ) & & should_add_sheet_if_empty )
m_workbook - > add_sheet ( " Sheet 1 " ) ;
2020-08-24 17:23:22 -03:00
2020-12-20 08:39:57 -03:00
m_tab_context_menu = GUI : : Menu : : construct ( ) ;
auto rename_action = GUI : : Action : : create ( " Rename... " , [ this ] ( auto & ) {
2021-02-23 16:42:32 -03:00
VERIFY ( m_tab_context_menu_sheet_view ) ;
2020-12-20 08:39:57 -03:00
2021-03-22 10:02:21 -03:00
auto * sheet_ptr = m_tab_context_menu_sheet_view - > sheet_if_available ( ) ;
VERIFY ( sheet_ptr ) ; // How did we get here without a sheet?
auto & sheet = * sheet_ptr ;
2020-12-20 08:39:57 -03:00
String new_name ;
2021-02-20 08:03:28 -03:00
if ( GUI : : InputBox : : show ( window ( ) , new_name , String : : formatted ( " New name for '{}' " , sheet . name ( ) ) , " Rename sheet " ) = = GUI : : Dialog : : ExecOK ) {
2020-12-20 08:39:57 -03:00
sheet . set_name ( new_name ) ;
sheet . update ( ) ;
m_tab_widget - > set_tab_title ( static_cast < GUI : : Widget & > ( * m_tab_context_menu_sheet_view ) , new_name ) ;
}
} ) ;
m_tab_context_menu - > add_action ( rename_action ) ;
m_tab_context_menu - > add_action ( GUI : : Action : : create ( " Add new sheet... " , [ this ] ( auto & ) {
String name ;
2021-02-20 08:03:28 -03:00
if ( GUI : : InputBox : : show ( window ( ) , name , " Name for new sheet " , " Create sheet " ) = = GUI : : Dialog : : ExecOK ) {
2020-12-20 08:39:57 -03:00
NonnullRefPtrVector < Sheet > new_sheets ;
new_sheets . append ( m_workbook - > add_sheet ( name ) ) ;
setup_tabs ( move ( new_sheets ) ) ;
}
} ) ) ;
2020-08-26 00:02:38 -03:00
setup_tabs ( m_workbook - > sheets ( ) ) ;
2020-08-24 17:23:22 -03:00
}
2020-12-28 17:03:16 -03:00
void SpreadsheetWidget : : resize_event ( GUI : : ResizeEvent & event )
{
GUI : : Widget : : resize_event ( event ) ;
if ( m_inline_documentation_window & & m_cell_value_editor & & window ( ) )
m_inline_documentation_window - > set_rect ( m_cell_value_editor - > screen_relative_rect ( ) . translated ( 0 , m_cell_value_editor - > height ( ) + 7 ) . inflated ( 6 , 6 ) ) ;
}
2020-08-26 00:02:38 -03:00
void SpreadsheetWidget : : setup_tabs ( NonnullRefPtrVector < Sheet > new_sheets )
2020-08-24 17:23:22 -03:00
{
RefPtr < GUI : : Widget > first_tab_widget ;
2020-08-26 00:02:38 -03:00
for ( auto & sheet : new_sheets ) {
2020-08-24 17:23:22 -03:00
auto & tab = m_tab_widget - > add_tab < SpreadsheetView > ( sheet . name ( ) , sheet ) ;
if ( ! first_tab_widget )
first_tab_widget = & tab ;
}
2020-08-24 12:21:25 -03:00
auto change = [ & ] ( auto & selected_widget ) {
if ( m_selected_view ) {
m_selected_view - > on_selection_changed = nullptr ;
m_selected_view - > on_selection_dropped = nullptr ;
2021-03-22 10:02:21 -03:00
}
2020-08-24 12:21:25 -03:00
m_selected_view = & static_cast < SpreadsheetView & > ( selected_widget ) ;
2020-08-27 04:26:26 -03:00
m_selected_view - > on_selection_changed = [ & ] ( Vector < Position > & & selection ) {
2021-03-22 10:02:21 -03:00
auto * sheet_ptr = m_selected_view - > sheet_if_available ( ) ;
// How did this even happen?
VERIFY ( sheet_ptr ) ;
auto & sheet = * sheet_ptr ;
2021-01-02 03:15:21 -03:00
if ( selection . is_empty ( ) ) {
m_current_cell_label - > set_enabled ( false ) ;
m_current_cell_label - > set_text ( { } ) ;
m_cell_value_editor - > on_change = nullptr ;
m_cell_value_editor - > on_focusin = nullptr ;
m_cell_value_editor - > on_focusout = nullptr ;
m_cell_value_editor - > set_text ( " " ) ;
m_cell_value_editor - > set_enabled ( false ) ;
return ;
}
2020-08-27 04:26:26 -03:00
if ( selection . size ( ) = = 1 ) {
auto & position = selection . first ( ) ;
m_current_cell_label - > set_enabled ( true ) ;
2021-03-22 10:02:21 -03:00
m_current_cell_label - > set_text ( position . to_cell_identifier ( sheet ) ) ;
2020-08-27 04:26:26 -03:00
2021-03-22 10:02:21 -03:00
auto & cell = sheet . ensure ( position ) ;
2020-08-27 04:26:26 -03:00
m_cell_value_editor - > on_change = nullptr ;
m_cell_value_editor - > set_text ( cell . source ( ) ) ;
m_cell_value_editor - > on_change = [ & ] {
2020-12-28 17:03:16 -03:00
auto text = m_cell_value_editor - > text ( ) ;
// FIXME: Lines?
auto offset = m_cell_value_editor - > cursor ( ) . column ( ) ;
try_generate_tip_for_input_expression ( text , offset ) ;
cell . set_data ( move ( text ) ) ;
2021-03-22 10:02:21 -03:00
sheet . update ( ) ;
2020-11-07 16:48:11 -03:00
update ( ) ;
2020-08-27 04:26:26 -03:00
} ;
m_cell_value_editor - > set_enabled ( true ) ;
2021-02-07 11:15:10 -03:00
static_cast < CellSyntaxHighlighter * > ( const_cast < Syntax : : Highlighter * > ( m_cell_value_editor - > syntax_highlighter ( ) ) ) - > set_cell ( & cell ) ;
2020-08-27 04:26:26 -03:00
return ;
}
// There are many cells selected, change all of them.
2020-08-24 12:21:25 -03:00
StringBuilder builder ;
2020-10-06 10:02:05 -03:00
builder . appendff ( " <{}> " , selection . size ( ) ) ;
2020-08-24 17:23:22 -03:00
m_current_cell_label - > set_enabled ( true ) ;
m_current_cell_label - > set_text ( builder . string_view ( ) ) ;
2020-08-24 12:21:25 -03:00
2021-06-08 12:06:27 -03:00
Vector < Cell & > cells ;
2020-08-27 04:26:26 -03:00
for ( auto & position : selection )
2021-06-08 12:06:27 -03:00
cells . append ( sheet . ensure ( position ) ) ;
2020-08-27 04:26:26 -03:00
2021-06-08 12:06:27 -03:00
auto & first_cell = cells . first ( ) ;
2020-08-24 17:23:22 -03:00
m_cell_value_editor - > on_change = nullptr ;
2020-08-27 04:26:26 -03:00
m_cell_value_editor - > set_text ( " " ) ;
2020-08-28 03:40:24 -03:00
m_should_change_selected_cells = false ;
m_cell_value_editor - > on_focusin = [ this ] { m_should_change_selected_cells = true ; } ;
m_cell_value_editor - > on_focusout = [ this ] { m_should_change_selected_cells = false ; } ;
2020-08-27 04:26:26 -03:00
m_cell_value_editor - > on_change = [ cells = move ( cells ) , this ] {
2020-08-28 03:40:24 -03:00
if ( m_should_change_selected_cells ) {
2021-03-22 10:02:21 -03:00
auto * sheet_ptr = m_selected_view - > sheet_if_available ( ) ;
if ( ! sheet_ptr )
return ;
auto & sheet = * sheet_ptr ;
2020-12-28 17:03:16 -03:00
auto text = m_cell_value_editor - > text ( ) ;
// FIXME: Lines?
auto offset = m_cell_value_editor - > cursor ( ) . column ( ) ;
try_generate_tip_for_input_expression ( text , offset ) ;
2021-06-08 12:06:27 -03:00
for ( auto & cell : cells )
cell . set_data ( text ) ;
2021-03-22 10:02:21 -03:00
sheet . update ( ) ;
2020-11-07 16:48:11 -03:00
update ( ) ;
2020-08-28 03:40:24 -03:00
}
2020-08-24 12:21:25 -03:00
} ;
2020-08-24 17:23:22 -03:00
m_cell_value_editor - > set_enabled ( true ) ;
2021-06-08 12:06:27 -03:00
static_cast < CellSyntaxHighlighter * > ( const_cast < Syntax : : Highlighter * > ( m_cell_value_editor - > syntax_highlighter ( ) ) ) - > set_cell ( & first_cell ) ;
2020-08-24 12:21:25 -03:00
} ;
m_selected_view - > on_selection_dropped = [ & ] ( ) {
2020-08-24 17:23:22 -03:00
m_cell_value_editor - > set_enabled ( false ) ;
2021-02-07 11:15:10 -03:00
static_cast < CellSyntaxHighlighter * > ( const_cast < Syntax : : Highlighter * > ( m_cell_value_editor - > syntax_highlighter ( ) ) ) - > set_cell ( nullptr ) ;
2020-08-24 17:23:22 -03:00
m_cell_value_editor - > set_text ( " " ) ;
m_current_cell_label - > set_enabled ( false ) ;
m_current_cell_label - > set_text ( " " ) ;
2020-08-24 12:21:25 -03:00
} ;
} ;
2020-08-24 17:23:22 -03:00
if ( first_tab_widget )
change ( * first_tab_widget ) ;
2020-08-24 12:21:25 -03:00
m_tab_widget - > on_change = [ change = move ( change ) ] ( auto & selected_widget ) {
change ( selected_widget ) ;
} ;
2020-12-20 08:39:57 -03:00
m_tab_widget - > on_context_menu_request = [ & ] ( auto & widget , auto & event ) {
2021-09-03 14:11:51 -03:00
m_tab_context_menu_sheet_view = static_cast < SpreadsheetView & > ( widget ) ;
2020-12-20 08:39:57 -03:00
m_tab_context_menu - > popup ( event . screen_position ( ) ) ;
} ;
2020-08-23 00:55:16 -03:00
}
2020-12-28 17:03:16 -03:00
void SpreadsheetWidget : : try_generate_tip_for_input_expression ( StringView source , size_t cursor_offset )
{
2021-03-22 10:02:21 -03:00
auto * sheet_ptr = m_selected_view - > sheet_if_available ( ) ;
if ( ! sheet_ptr )
return ;
auto & sheet = * sheet_ptr ;
2020-12-28 17:03:16 -03:00
m_inline_documentation_window - > set_rect ( m_cell_value_editor - > screen_relative_rect ( ) . translated ( 0 , m_cell_value_editor - > height ( ) + 7 ) . inflated ( 6 , 6 ) ) ;
if ( ! m_selected_view | | ! source . starts_with ( ' = ' ) ) {
m_inline_documentation_window - > hide ( ) ;
return ;
}
auto maybe_function_and_argument = get_function_and_argument_index ( source . substring_view ( 0 , cursor_offset ) ) ;
if ( ! maybe_function_and_argument . has_value ( ) ) {
m_inline_documentation_window - > hide ( ) ;
return ;
}
auto & [ name , index ] = maybe_function_and_argument . value ( ) ;
auto text = sheet . generate_inline_documentation_for ( name , index ) ;
if ( text . is_empty ( ) ) {
m_inline_documentation_window - > hide ( ) ;
} else {
m_inline_documentation_label - > set_text ( move ( text ) ) ;
m_inline_documentation_window - > show ( ) ;
}
}
2020-08-25 22:30:23 -03:00
void SpreadsheetWidget : : save ( const StringView & filename )
2020-08-24 17:36:32 -03:00
{
2020-08-25 22:30:23 -03:00
auto result = m_workbook - > save ( filename ) ;
if ( result . is_error ( ) )
GUI : : MessageBox : : show_error ( window ( ) , result . error ( ) ) ;
2020-08-24 17:36:32 -03:00
}
2020-08-24 17:23:22 -03:00
void SpreadsheetWidget : : load ( const StringView & filename )
{
2020-08-25 22:30:23 -03:00
auto result = m_workbook - > load ( filename ) ;
if ( result . is_error ( ) ) {
GUI : : MessageBox : : show_error ( window ( ) , result . error ( ) ) ;
2020-08-24 17:23:22 -03:00
return ;
}
2020-11-29 10:57:08 -03:00
m_tab_widget - > on_change = nullptr ;
m_cell_value_editor - > on_change = nullptr ;
m_current_cell_label - > set_text ( " " ) ;
m_should_change_selected_cells = false ;
2020-08-24 17:23:22 -03:00
while ( auto * widget = m_tab_widget - > active_widget ( ) ) {
m_tab_widget - > remove_tab ( * widget ) ;
}
2020-08-26 00:02:38 -03:00
setup_tabs ( m_workbook - > sheets ( ) ) ;
}
2020-12-22 02:15:15 -03:00
bool SpreadsheetWidget : : request_close ( )
{
if ( ! m_workbook - > dirty ( ) )
return true ;
auto result = GUI : : MessageBox : : show ( window ( ) , " The spreadsheet has been modified. Would you like to save? " , " Unsaved changes " , GUI : : MessageBox : : Type : : Warning , GUI : : MessageBox : : InputType : : YesNoCancel ) ;
if ( result = = GUI : : MessageBox : : ExecYes ) {
if ( current_filename ( ) . is_empty ( ) ) {
String name = " workbook " ;
Optional < String > save_path = GUI : : FilePicker : : get_save_filepath ( window ( ) , name , " sheets " ) ;
if ( ! save_path . has_value ( ) )
return false ;
save ( save_path . value ( ) ) ;
} else {
save ( current_filename ( ) ) ;
}
return true ;
}
if ( result = = GUI : : MessageBox : : ExecNo )
return true ;
return false ;
}
2020-08-26 00:02:38 -03:00
void SpreadsheetWidget : : add_sheet ( )
{
StringBuilder name ;
name . append ( " Sheet " ) ;
2020-10-06 10:02:05 -03:00
name . appendff ( " {} " , m_workbook - > sheets ( ) . size ( ) + 1 ) ;
2020-08-26 00:02:38 -03:00
2020-10-31 10:22:13 -03:00
NonnullRefPtrVector < Sheet > new_sheets ;
new_sheets . append ( m_workbook - > add_sheet ( name . string_view ( ) ) ) ;
setup_tabs ( move ( new_sheets ) ) ;
}
void SpreadsheetWidget : : add_sheet ( NonnullRefPtr < Sheet > & & sheet )
{
2021-02-23 16:42:32 -03:00
VERIFY ( m_workbook = = & sheet - > workbook ( ) ) ;
2020-08-26 00:02:38 -03:00
NonnullRefPtrVector < Sheet > new_sheets ;
2020-10-31 10:22:13 -03:00
new_sheets . append ( move ( sheet ) ) ;
2021-06-12 08:24:45 -03:00
m_workbook - > sheets ( ) . extend ( new_sheets ) ;
2020-08-26 00:02:38 -03:00
setup_tabs ( new_sheets ) ;
2020-08-24 17:23:22 -03:00
}
2020-08-25 22:30:23 -03:00
void SpreadsheetWidget : : set_filename ( const String & filename )
2020-08-23 00:55:16 -03:00
{
2020-08-25 22:30:23 -03:00
if ( m_workbook - > set_filename ( filename ) ) {
StringBuilder builder ;
builder . append ( " Spreadsheet - " ) ;
builder . append ( current_filename ( ) ) ;
2020-08-23 00:55:16 -03:00
2020-08-25 22:30:23 -03:00
window ( ) - > set_title ( builder . string_view ( ) ) ;
window ( ) - > update ( ) ;
2020-08-23 00:55:16 -03:00
}
}
2020-08-25 22:30:23 -03:00
SpreadsheetWidget : : ~ SpreadsheetWidget ( )
{
}
2020-08-23 00:55:16 -03:00
}