2021-01-06 15:40:49 -03:00
/*
2022-02-09 19:51:48 -03:00
* Copyright ( c ) 2021 - 2022 , the SerenityOS developers .
2021-01-06 15:40:49 -03:00
*
2021-04-22 05:24:48 -03:00
* SPDX - License - Identifier : BSD - 2 - Clause
2021-01-06 15:40:49 -03:00
*/
2021-05-14 20:27:09 -03:00
2021-01-06 15:40:49 -03:00
# include "TreeMapWidget.h"
2021-03-01 18:27:37 -03:00
# include <AK/LexicalPath.h>
2021-01-06 15:40:49 -03:00
# include <AK/Queue.h>
# include <AK/QuickSort.h>
2022-02-09 21:45:15 -03:00
# include <AK/StringView.h>
2021-02-20 12:02:28 -03:00
# include <AK/URL.h>
2021-01-06 15:40:49 -03:00
# include <Applications/SpaceAnalyzer/SpaceAnalyzerGML.h>
# include <LibCore/DirIterator.h>
# include <LibCore/File.h>
2021-02-20 12:02:28 -03:00
# include <LibDesktop/Launcher.h>
2021-01-06 15:40:49 -03:00
# include <LibGUI/Application.h>
2021-10-30 02:09:43 -03:00
# include <LibGUI/BoxLayout.h>
2021-04-13 11:18:20 -03:00
# include <LibGUI/Breadcrumbbar.h>
2021-02-20 12:02:28 -03:00
# include <LibGUI/Clipboard.h>
2021-09-01 15:39:38 -03:00
# include <LibGUI/FileIconProvider.h>
2021-01-06 15:40:49 -03:00
# include <LibGUI/Icon.h>
2021-10-30 02:09:43 -03:00
# include <LibGUI/Label.h>
2021-01-06 15:40:49 -03:00
# include <LibGUI/Menu.h>
2021-04-13 11:18:20 -03:00
# include <LibGUI/Menubar.h>
2021-02-20 12:02:28 -03:00
# include <LibGUI/MessageBox.h>
2021-04-13 11:18:20 -03:00
# include <LibGUI/Statusbar.h>
2022-02-09 19:51:48 -03:00
# include <LibMain/Main.h>
2021-05-14 16:29:21 -03:00
# include <fcntl.h>
2021-01-06 15:40:49 -03:00
# include <sys/stat.h>
2021-03-12 13:29:37 -03:00
# include <unistd.h>
2021-01-06 15:40:49 -03:00
2022-07-11 14:32:29 -03:00
static constexpr auto APP_NAME = " Space Analyzer " sv ;
2021-10-30 02:09:43 -03:00
static constexpr size_t FILES_ENCOUNTERED_UPDATE_STEP_SIZE = 25 ;
2021-05-21 06:30:21 -03:00
2021-01-06 15:40:49 -03:00
struct TreeNode : public SpaceAnalyzer : : TreeMapNode {
TreeNode ( String name )
: m_name ( move ( name ) ) { } ;
2021-12-04 06:09:09 -03:00
virtual String name ( ) const override { return m_name ; }
virtual i64 area ( ) const override { return m_area ; }
virtual size_t num_children ( ) const override
2021-01-06 15:40:49 -03:00
{
if ( m_children ) {
return m_children - > size ( ) ;
}
return 0 ;
}
2022-04-01 14:58:27 -03:00
virtual TreeNode const & child_at ( size_t i ) const override { return m_children - > at ( i ) ; }
2021-12-04 06:09:09 -03:00
virtual void sort_children_by_area ( ) const override
2021-01-06 15:40:49 -03:00
{
if ( m_children ) {
Vector < TreeNode > * children = const_cast < Vector < TreeNode > * > ( m_children . ptr ( ) ) ;
quick_sort ( * children , [ ] ( auto & a , auto & b ) { return b . m_area < a . m_area ; } ) ;
}
}
String m_name ;
2021-10-06 09:27:25 -03:00
i64 m_area { 0 } ;
2021-01-06 15:40:49 -03:00
OwnPtr < Vector < TreeNode > > m_children ;
} ;
struct Tree : public SpaceAnalyzer : : TreeMap {
Tree ( String root_name )
: m_root ( move ( root_name ) ) { } ;
virtual ~ Tree ( ) { } ;
TreeNode m_root ;
2022-04-01 14:58:27 -03:00
virtual SpaceAnalyzer : : TreeMapNode const & root ( ) const override
2021-01-06 15:40:49 -03:00
{
return m_root ;
} ;
} ;
struct MountInfo {
String mount_point ;
String source ;
} ;
static void fill_mounts ( Vector < MountInfo > & output )
{
// Output info about currently mounted filesystems.
2021-05-30 15:25:04 -03:00
auto file = Core : : File : : construct ( " /proc/df " ) ;
if ( ! file - > open ( Core : : OpenMode : : ReadOnly ) ) {
warnln ( " Failed to open {}: {} " , file - > name ( ) , file - > error_string ( ) ) ;
2021-01-06 15:40:49 -03:00
return ;
}
2021-05-30 15:25:04 -03:00
auto content = file - > read_all ( ) ;
2021-11-14 21:46:51 -03:00
auto json = JsonValue : : from_string ( content ) . release_value_but_fixme_should_propagate_errors ( ) ;
2021-01-06 15:40:49 -03:00
2022-07-11 14:32:29 -03:00
json . as_array ( ) . for_each ( [ & output ] ( JsonValue const & value ) {
2021-05-31 13:59:02 -03:00
auto & filesystem_object = value . as_object ( ) ;
2021-01-06 15:40:49 -03:00
MountInfo mount_info ;
2022-07-11 14:32:29 -03:00
mount_info . mount_point = filesystem_object . get ( " mount_point " sv ) . to_string ( ) ;
mount_info . source = filesystem_object . get ( " source " sv ) . as_string_or ( " none " sv ) ;
2021-01-06 15:40:49 -03:00
output . append ( mount_info ) ;
} ) ;
}
static MountInfo * find_mount_for_path ( String path , Vector < MountInfo > & mounts )
{
MountInfo * result = nullptr ;
size_t length = 0 ;
for ( auto & mount_info : mounts ) {
String & mount_point = mount_info . mount_point ;
if ( path . starts_with ( mount_point ) ) {
if ( ! result | | mount_point . length ( ) > length ) {
result = & mount_info ;
length = mount_point . length ( ) ;
}
}
}
return result ;
}
static long long int update_totals ( TreeNode & node )
{
long long int result = 0 ;
if ( node . m_children ) {
for ( auto & child : * node . m_children ) {
result + = update_totals ( child ) ;
}
node . m_area = result ;
} else {
result = node . m_area ;
}
return result ;
}
2021-10-30 02:09:43 -03:00
static NonnullRefPtr < GUI : : Window > create_progress_window ( )
{
auto window = GUI : : Window : : construct ( ) ;
window - > set_title ( APP_NAME ) ;
window - > set_resizable ( false ) ;
window - > set_closeable ( false ) ;
window - > resize ( 240 , 50 ) ;
window - > center_on_screen ( ) ;
auto & main_widget = window - > set_main_widget < GUI : : Widget > ( ) ;
main_widget . set_fill_with_background_color ( true ) ;
main_widget . set_layout < GUI : : VerticalBoxLayout > ( ) ;
auto & label = main_widget . add < GUI : : Label > ( " Analyzing storage space... " ) ;
label . set_fixed_height ( 22 ) ;
auto & progresslabel = main_widget . add < GUI : : Label > ( ) ;
progresslabel . set_name ( " progresslabel " ) ;
progresslabel . set_fixed_height ( 22 ) ;
return window ;
}
static void update_progress_label ( GUI : : Label & progresslabel , size_t files_encountered_count )
{
auto text = String : : formatted ( " {} files... " , files_encountered_count ) ;
progresslabel . set_text ( text ) ;
Core : : EventLoop : : current ( ) . pump ( Core : : EventLoop : : WaitMode : : PollForEvents ) ;
}
2021-01-06 15:40:49 -03:00
struct QueueEntry {
QueueEntry ( String path , TreeNode * node )
: path ( move ( path ) )
, node ( node ) { } ;
String path ;
TreeNode * node { nullptr } ;
} ;
2021-10-30 02:09:43 -03:00
static void populate_filesize_tree ( TreeNode & root , Vector < MountInfo > & mounts , HashMap < int , int > & error_accumulator , GUI : : Label & progresslabel )
2021-01-06 15:40:49 -03:00
{
2022-07-11 17:10:18 -03:00
VERIFY ( ! root . m_name . ends_with ( ' / ' ) ) ;
2021-01-06 15:40:49 -03:00
Queue < QueueEntry > queue ;
queue . enqueue ( QueueEntry ( root . m_name , & root ) ) ;
2021-10-30 02:09:43 -03:00
size_t files_encountered_count = 0 ;
2021-01-06 15:40:49 -03:00
StringBuilder builder = StringBuilder ( ) ;
builder . append ( root . m_name ) ;
2022-07-11 17:10:18 -03:00
builder . append ( ' / ' ) ;
2021-01-06 15:40:49 -03:00
MountInfo * root_mount_info = find_mount_for_path ( builder . to_string ( ) , mounts ) ;
if ( ! root_mount_info ) {
return ;
}
while ( ! queue . is_empty ( ) ) {
QueueEntry queue_entry = queue . dequeue ( ) ;
builder . clear ( ) ;
builder . append ( queue_entry . path ) ;
2022-07-11 17:10:18 -03:00
builder . append ( ' / ' ) ;
2021-01-06 15:40:49 -03:00
MountInfo * mount_info = find_mount_for_path ( builder . to_string ( ) , mounts ) ;
if ( ! mount_info | | ( mount_info ! = root_mount_info & & mount_info - > source ! = root_mount_info - > source ) ) {
continue ;
}
Core : : DirIterator dir_iterator ( builder . to_string ( ) , Core : : DirIterator : : SkipParentAndBaseDir ) ;
if ( dir_iterator . has_error ( ) ) {
int error_sum = error_accumulator . get ( dir_iterator . error ( ) ) . value_or ( 0 ) ;
error_accumulator . set ( dir_iterator . error ( ) , error_sum + 1 ) ;
} else {
queue_entry . node - > m_children = make < Vector < TreeNode > > ( ) ;
while ( dir_iterator . has_next ( ) ) {
queue_entry . node - > m_children - > append ( TreeNode ( dir_iterator . next_path ( ) ) ) ;
}
for ( auto & child : * queue_entry . node - > m_children ) {
2021-10-30 02:09:43 -03:00
files_encountered_count + = 1 ;
if ( ! ( files_encountered_count % FILES_ENCOUNTERED_UPDATE_STEP_SIZE ) )
update_progress_label ( progresslabel , files_encountered_count ) ;
2021-01-06 15:40:49 -03:00
String & name = child . m_name ;
int name_len = name . length ( ) ;
builder . append ( name ) ;
struct stat st ;
2021-05-14 16:29:21 -03:00
int stat_result = fstatat ( dir_iterator . fd ( ) , name . characters ( ) , & st , AT_SYMLINK_NOFOLLOW ) ;
2021-01-06 15:40:49 -03:00
if ( stat_result < 0 ) {
int error_sum = error_accumulator . get ( errno ) . value_or ( 0 ) ;
error_accumulator . set ( errno , error_sum + 1 ) ;
} else {
if ( S_ISDIR ( st . st_mode ) ) {
queue . enqueue ( QueueEntry ( builder . to_string ( ) , & child ) ) ;
} else {
child . m_area = st . st_size ;
}
}
builder . trim ( name_len ) ;
}
}
}
update_totals ( root ) ;
}
2021-04-13 11:18:20 -03:00
static void analyze ( RefPtr < Tree > tree , SpaceAnalyzer : : TreeMapWidget & treemapwidget , GUI : : Statusbar & statusbar )
2021-01-06 15:40:49 -03:00
{
2021-10-30 02:09:43 -03:00
statusbar . set_text ( " " ) ;
auto progress_window = create_progress_window ( ) ;
progress_window - > show ( ) ;
auto & progresslabel = * progress_window - > main_widget ( ) - > find_descendant_of_type_named < GUI : : Label > ( " progresslabel " ) ;
update_progress_label ( progresslabel , 0 ) ;
2021-01-06 15:40:49 -03:00
// Build an in-memory tree mirroring the filesystem and for each node
// calculate the sum of the file size for all its descendants.
TreeNode * root = & tree - > m_root ;
Vector < MountInfo > mounts ;
fill_mounts ( mounts ) ;
HashMap < int , int > error_accumulator ;
2021-10-30 02:09:43 -03:00
populate_filesize_tree ( * root , mounts , error_accumulator , progresslabel ) ;
progress_window - > close ( ) ;
2021-01-06 15:40:49 -03:00
// Display an error summary in the statusbar.
if ( ! error_accumulator . is_empty ( ) ) {
StringBuilder builder ;
bool first = true ;
2022-07-11 14:32:29 -03:00
builder . append ( " Some directories were not analyzed: " sv ) ;
2021-01-06 15:40:49 -03:00
for ( auto & key : error_accumulator . keys ( ) ) {
if ( ! first ) {
2022-07-11 14:32:29 -03:00
builder . append ( " , " sv ) ;
2021-01-06 15:40:49 -03:00
}
2022-07-11 16:53:29 -03:00
auto const * error = strerror ( key ) ;
builder . append ( { error , strlen ( error ) } ) ;
2022-07-11 14:32:29 -03:00
builder . append ( " ( " sv ) ;
2021-01-06 15:40:49 -03:00
int value = error_accumulator . get ( key ) . value ( ) ;
builder . append ( String : : number ( value ) ) ;
if ( value = = 1 ) {
2022-07-11 14:32:29 -03:00
builder . append ( " time " sv ) ;
2021-01-06 15:40:49 -03:00
} else {
2022-07-11 14:32:29 -03:00
builder . append ( " times " sv ) ;
2021-01-06 15:40:49 -03:00
}
2022-07-11 17:10:18 -03:00
builder . append ( ' ) ' ) ;
2021-01-06 15:40:49 -03:00
first = false ;
}
statusbar . set_text ( builder . to_string ( ) ) ;
} else {
statusbar . set_text ( " No errors " ) ;
}
treemapwidget . set_tree ( tree ) ;
}
2022-04-01 14:58:27 -03:00
static bool is_removable ( String const & absolute_path )
2021-02-20 12:02:28 -03:00
{
2021-02-23 16:42:32 -03:00
VERIFY ( ! absolute_path . is_empty ( ) ) ;
2021-10-07 16:59:22 -03:00
int access_result = access ( LexicalPath : : dirname ( absolute_path ) . characters ( ) , W_OK ) ;
2021-02-20 12:02:28 -03:00
if ( access_result ! = 0 & & errno ! = EACCES )
perror ( " access " ) ;
return access_result = = 0 ;
}
2022-04-01 14:58:27 -03:00
static String get_absolute_path_to_selected_node ( SpaceAnalyzer : : TreeMapWidget const & treemapwidget , bool include_last_node = true )
2021-02-20 12:02:28 -03:00
{
StringBuilder path_builder ;
for ( size_t k = 0 ; k < treemapwidget . path_size ( ) - ( include_last_node ? 0 : 1 ) ; k + + ) {
if ( k ! = 0 ) {
path_builder . append ( ' / ' ) ;
}
2022-04-01 14:58:27 -03:00
SpaceAnalyzer : : TreeMapNode const * node = treemapwidget . path_node ( k ) ;
2021-02-20 12:02:28 -03:00
path_builder . append ( node - > name ( ) ) ;
}
return path_builder . build ( ) ;
}
2022-02-09 19:51:48 -03:00
ErrorOr < int > serenity_main ( Main : : Arguments arguments )
2021-01-06 15:40:49 -03:00
{
2022-04-16 13:17:39 -03:00
auto app = TRY ( GUI : : Application : : try_create ( arguments ) ) ;
2021-01-06 15:40:49 -03:00
2021-04-23 11:46:57 -03:00
RefPtr < Tree > tree = adopt_ref ( * new Tree ( " " ) ) ;
2021-01-06 15:40:49 -03:00
// Configure application window.
2022-07-11 14:32:29 -03:00
auto app_icon = GUI : : Icon : : default_icon ( " app-space-analyzer " sv ) ;
2021-01-06 15:40:49 -03:00
auto window = GUI : : Window : : construct ( ) ;
window - > set_title ( APP_NAME ) ;
window - > resize ( 640 , 480 ) ;
window - > set_icon ( app_icon . bitmap_for_size ( 16 ) ) ;
// Load widgets.
auto & mainwidget = window - > set_main_widget < GUI : : Widget > ( ) ;
mainwidget . load_from_gml ( space_analyzer_gml ) ;
2021-04-13 11:18:20 -03:00
auto & breadcrumbbar = * mainwidget . find_descendant_of_type_named < GUI : : Breadcrumbbar > ( " breadcrumbbar " ) ;
2021-01-06 15:40:49 -03:00
auto & treemapwidget = * mainwidget . find_descendant_of_type_named < SpaceAnalyzer : : TreeMapWidget > ( " tree_map " ) ;
2021-04-13 11:18:20 -03:00
auto & statusbar = * mainwidget . find_descendant_of_type_named < GUI : : Statusbar > ( " statusbar " ) ;
2021-01-06 15:40:49 -03:00
2022-03-30 16:44:00 -03:00
treemapwidget . set_focus ( true ) ;
2021-07-21 16:21:03 -03:00
auto & file_menu = window - > add_menu ( " &File " ) ;
2021-05-01 05:45:39 -03:00
file_menu . add_action ( GUI : : Action : : create ( " &Analyze " , [ & ] ( auto & ) {
2021-01-06 15:40:49 -03:00
analyze ( tree , treemapwidget , statusbar ) ;
} ) ) ;
2021-05-14 20:24:07 -03:00
file_menu . add_separator ( ) ;
2021-05-01 05:45:39 -03:00
file_menu . add_action ( GUI : : CommonActions : : make_quit_action ( [ & ] ( auto & ) {
2021-01-06 15:40:49 -03:00
app - > quit ( ) ;
} ) ) ;
2021-05-12 14:09:42 -03:00
2021-07-21 16:21:03 -03:00
auto & help_menu = window - > add_menu ( " &Help " ) ;
2021-01-06 15:40:49 -03:00
help_menu . add_action ( GUI : : CommonActions : : make_about_action ( APP_NAME , app_icon , window ) ) ;
2021-05-12 14:09:42 -03:00
2021-02-20 12:02:28 -03:00
// Configure the nodes context menu.
2022-07-11 14:32:29 -03:00
auto open_folder_action = GUI : : Action : : create ( " Open Folder " , { Mod_Ctrl , Key_O } , Gfx : : Bitmap : : try_load_from_file ( " /res/icons/16x16/open.png " sv ) . release_value_but_fixme_should_propagate_errors ( ) , [ & ] ( auto & ) {
2021-02-20 12:02:28 -03:00
Desktop : : Launcher : : open ( URL : : create_with_file_protocol ( get_absolute_path_to_selected_node ( treemapwidget ) ) ) ;
} ) ;
2022-07-11 14:32:29 -03:00
auto open_containing_folder_action = GUI : : Action : : create ( " Open Containing Folder " , { Mod_Ctrl , Key_O } , Gfx : : Bitmap : : try_load_from_file ( " /res/icons/16x16/open.png " sv ) . release_value_but_fixme_should_propagate_errors ( ) , [ & ] ( auto & ) {
2021-03-01 18:27:37 -03:00
LexicalPath path { get_absolute_path_to_selected_node ( treemapwidget ) } ;
Desktop : : Launcher : : open ( URL : : create_with_file_protocol ( path . dirname ( ) , path . basename ( ) ) ) ;
2021-02-20 12:02:28 -03:00
} ) ;
2022-07-11 14:32:29 -03:00
auto copy_path_action = GUI : : Action : : create ( " Copy Path to Clipboard " , { Mod_Ctrl , Key_C } , Gfx : : Bitmap : : try_load_from_file ( " /res/icons/16x16/edit-copy.png " sv ) . release_value_but_fixme_should_propagate_errors ( ) , [ & ] ( auto & ) {
2021-02-20 12:02:28 -03:00
GUI : : Clipboard : : the ( ) . set_plain_text ( get_absolute_path_to_selected_node ( treemapwidget ) ) ;
} ) ;
auto delete_action = GUI : : CommonActions : : make_delete_action ( [ & ] ( auto & ) {
String selected_node_path = get_absolute_path_to_selected_node ( treemapwidget ) ;
bool try_again = true ;
while ( try_again ) {
try_again = false ;
auto deletion_result = Core : : File : : remove ( selected_node_path , Core : : File : : RecursionMode : : Allowed , true ) ;
if ( deletion_result . is_error ( ) ) {
auto retry_message_result = GUI : : MessageBox : : show ( window ,
String : : formatted ( " Failed to delete \" {} \" : {}. Retry? " ,
deletion_result . error ( ) . file ,
2021-11-06 21:31:00 -03:00
static_cast < Error const & > ( deletion_result . error ( ) ) ) ,
2022-07-11 14:32:29 -03:00
" Deletion failed " sv ,
2021-02-20 12:02:28 -03:00
GUI : : MessageBox : : Type : : Error ,
GUI : : MessageBox : : InputType : : YesNo ) ;
2022-05-13 09:10:27 -03:00
if ( retry_message_result = = GUI : : MessageBox : : ExecResult : : Yes ) {
2021-02-20 12:02:28 -03:00
try_again = true ;
}
} else {
GUI : : MessageBox : : show ( window ,
2021-04-18 05:30:03 -03:00
String : : formatted ( " Successfully deleted \" {} \" . " , selected_node_path ) ,
2022-07-11 14:32:29 -03:00
" Deletion completed " sv ,
2021-02-20 12:02:28 -03:00
GUI : : MessageBox : : Type : : Information ,
GUI : : MessageBox : : InputType : : OK ) ;
}
}
// TODO: Refreshing data always causes resetting the viewport back to "/".
// It would be great if we found a way to preserve viewport across refreshes.
analyze ( tree , treemapwidget , statusbar ) ;
} ) ;
// TODO: Both these menus could've been implemented as one, but it's impossible to change action text after it's shown once.
auto folder_node_context_menu = GUI : : Menu : : construct ( ) ;
folder_node_context_menu - > add_action ( * open_folder_action ) ;
folder_node_context_menu - > add_action ( * copy_path_action ) ;
folder_node_context_menu - > add_action ( * delete_action ) ;
auto file_node_context_menu = GUI : : Menu : : construct ( ) ;
file_node_context_menu - > add_action ( * open_containing_folder_action ) ;
file_node_context_menu - > add_action ( * copy_path_action ) ;
file_node_context_menu - > add_action ( * delete_action ) ;
2021-01-06 15:40:49 -03:00
// Configure event handlers.
breadcrumbbar . on_segment_click = [ & ] ( size_t index ) {
2021-02-23 16:42:32 -03:00
VERIFY ( index < treemapwidget . path_size ( ) ) ;
2021-01-06 15:40:49 -03:00
treemapwidget . set_viewpoint ( index ) ;
} ;
treemapwidget . on_path_change = [ & ] ( ) {
2021-09-01 15:39:38 -03:00
StringBuilder builder ;
2021-01-06 15:40:49 -03:00
breadcrumbbar . clear_segments ( ) ;
for ( size_t k = 0 ; k < treemapwidget . path_size ( ) ; k + + ) {
if ( k = = 0 ) {
2021-09-01 15:39:38 -03:00
breadcrumbbar . append_segment ( " / " , GUI : : FileIconProvider : : icon_for_path ( " / " ) . bitmap_for_size ( 16 ) , " / " , " / " ) ;
continue ;
2021-01-06 15:40:49 -03:00
}
2021-09-01 15:39:38 -03:00
const SpaceAnalyzer : : TreeMapNode * node = treemapwidget . path_node ( k ) ;
2022-07-11 17:10:18 -03:00
builder . append ( ' / ' ) ;
2021-09-01 15:39:38 -03:00
builder . append ( node - > name ( ) ) ;
breadcrumbbar . append_segment ( node - > name ( ) , GUI : : FileIconProvider : : icon_for_path ( builder . string_view ( ) ) . bitmap_for_size ( 16 ) , builder . string_view ( ) , builder . string_view ( ) ) ;
2021-01-06 15:40:49 -03:00
}
breadcrumbbar . set_selected_segment ( treemapwidget . viewpoint ( ) ) ;
} ;
2021-02-20 12:02:28 -03:00
treemapwidget . on_context_menu_request = [ & ] ( const GUI : : ContextMenuEvent & event ) {
String selected_node_path = get_absolute_path_to_selected_node ( treemapwidget ) ;
if ( selected_node_path . is_empty ( ) )
return ;
delete_action - > set_enabled ( is_removable ( selected_node_path ) ) ;
if ( Core : : File : : is_directory ( selected_node_path ) ) {
folder_node_context_menu - > popup ( event . screen_position ( ) ) ;
} else {
file_node_context_menu - > popup ( event . screen_position ( ) ) ;
}
} ;
2021-01-06 15:40:49 -03:00
// At startup automatically do an analysis of root.
analyze ( tree , treemapwidget , statusbar ) ;
window - > show ( ) ;
return app - > exec ( ) ;
}