Module saePisan.view.MainWindow

Classes

class MainWindow
Expand source code
class MainWindow(QMainWindow):
    """Main application window for SAE Pisan: Small Area Estimation Programming for Statistical Analysis.
    Attributes:
        data1 (pl.DataFrame): DataFrame for the first sheet (Data Editor).
        data2 (pl.DataFrame): DataFrame for the second sheet (Data Output).
        model1 (TableModel): Table model for the first sheet.
        model2 (TableModel): Table model for the second sheet.
        path (str): Path to the application directory.
        font_size (int): Default font size for the application.
        show_modeling_sae_dialog (ModelingSaeDialog): Dialog for SAE modeling.
        show_modeling_saeHB_dialog (ModelingSaeHBDialog): Dialog for SAE HB modeling.
        show_modeling_sae_unit_dialog (ModelingSaeUnitDialog): Dialog for SAE unit modeling.
        show_modeling_saeHB_normal_dialog (ModelingSaeHBNormalDialog): Dialog for SAE HB normal modeling.
        show_modellig_sae_pseudo_dialog (ModelingSaePseudoDialog): Dialog for SAE pseudo modeling.
        show_compute_variable_dialog (ComputeVariableDialog): Dialog for computing new variables.
        show_projection_variabel_dialog (ProjectionDialog): Dialog for variable projection.
        menu_bar (QMenuBar): Menu bar for the application.
        toolBar (QToolBar): Tool bar for the application.
        tab_widget (QTabWidget): Tab widget containing the different sheets.
        spreadsheet (QTableView): Table view for the first sheet.
        table_view2 (QTableView): Table view for the second sheet.
        scroll_area (QScrollArea): Scroll area for the output tab.
        output_layout (QVBoxLayout): Layout for displaying output in the output tab.
    Methods:
        init_ui(): Initializes the user interface.
        change_font_size(): Opens a dialog to change the font size.
        set_font_size(size): Sets the font size for the application.
        load_stylesheet_with_font_size(size): Loads the stylesheet with the specified font size.
        open_summary_data_dialog_lazy(): Lazily opens the summary data dialog.
        open_normality_test_dialog_lazy(): Lazily opens the normality test dialog.
        open_multicollinearity_dialog_lazy(): Lazily opens the multicollinearity dialog.
        open_variable_selection_dialog_lazy(): Lazily opens the variable selection dialog.
        open_scatter_plot_dialog_lazy(): Lazily opens the scatter plot dialog.
        open_correlation_matrix_dialog_lazy(): Lazily opens the correlation matrix dialog.
        open_box_plot_dialog_lazy(): Lazily opens the box plot dialog.
        open_line_plot_dialog_lazy(): Lazily opens the line plot dialog.
        open_histogram_dialog_lazy(): Lazily opens the histogram dialog.
        open_summary_data_dialog(): Opens the summary data dialog.
        open_normality_test_dialog(): Opens the normality test dialog.
        open_scatter_plot_dialog(): Opens the scatter plot dialog.
        open_line_plot_dialog(): Opens the line plot dialog.
        open_box_plot_dialog(): Opens the box plot dialog.
        open_correlation_matrix_dialog(): Opens the correlation matrix dialog.
        open_multicollinearity_dialog(): Opens the multicollinearity dialog.
        open_histogram_dialog(): Opens the histogram dialog.
        open_variable_selection_dialog(): Opens the variable selection dialog.
        show_modeling_sae_dialog_lazy(): Lazily shows the SAE modeling dialog.
        show_modeling_saeHB_dialog_lazy(): Lazily shows the SAE HB modeling dialog.
        show_modeling_sae_unit_dialog_lazy(): Lazily shows the SAE unit modeling dialog.
        show_modeling_saeHB_normal_dialog_lazy(): Lazily shows the SAE HB normal modeling dialog.
        show_modellig_sae_pseudo_dialog_lazy(): Lazily shows the SAE pseudo modeling dialog.
        show_compute_variable_dialog_lazy(): Lazily shows the compute variable dialog.
        show_projection_variabel_dialog_lazy(): Lazily shows the projection variable dialog.
        open_about_dialog(): Opens the about dialog.
        add_row(sheet_number): Adds a new row to the specified sheet.
        add_column(sheet_number): Adds a new column to the specified sheet.
        update_table(sheet_number, model): Updates the table for the specified sheet with a new model.
        keyPressEvent(event): Handles keyboard shortcuts for copy, paste, undo, and redo.
        copy_selection(): Copies the selected cells to the clipboard.
        paste_selection(): Pastes the clipboard content to the selected cells.
        undo_action(): Undoes the last action.
        redo_action(): Redoes the last undone action.
        group_by_row(selection): Groups selected indexes by row.
        show_output(title, content): Displays output in the Output tab.
        show_header_context_menu(pos): Shows the context menu for the header.
        rename_column(column_index): Renames the column at the given index.
        edit_data_type(column_index): Edits the data type of the column at the given index.
        set_path(path): Sets the path for the application.
        add_output(script_text, result_text=None, plot_paths=None, error_text=None): Adds output to the layout in the form of a card.
        remove_output(card_frame): Removes output from the layout.
        copy_output_image(card_frame): Copies the output image to the clipboard.
        show_context_menu(pos, card_frame): Shows the context menu for each output."""
    
    def __init__(self):
        """
        Initializes the MainWindow class.
        This constructor sets up the main window for the SAE Pisan application, 
        including setting the window title, initializing data frames, models, 
        and UI components.
        Attributes:
            data1 (pl.DataFrame): A DataFrame with 100 columns and 100 empty rows.
            data2 (pl.DataFrame): A DataFrame with columns "Estimated Value", 
                                  "Standar Error", and "CV", each with 100 empty rows.
            model1 (TableModel): The table model for the first data frame.
            model2 (TableModel): The table model for the second data frame.
            path (str): The path to the parent directory of the current file.
            font_size (int): The font size used in the UI.
        """
        
        super().__init__()

        self.setWindowTitle("saePisan: Small Area Estimation Programming for Statistical Analysis v1.4.0")
        columns = [f"Column {i+1}" for i in range(100)]
        self.data1 = pl.DataFrame({col: [None] * 100 for col in columns})
        self.data2 = pl.DataFrame({
            "Estimated Value": [None] * 100,
            "Standar Error": [None] * 100,
            "RSE (%)": [None] * 100
        })

        # Model untuk Sheet 2
        self.model1 = TableModel(self.data1)
        self.model2 = TableModel(self.data2)
        self.path = os.path.join(os.path.dirname(__file__), '..')
        self.font_size = 12
        self.set_font_size(self.font_size)
        self.data = []

        # Inisialisasi UI
        self.init_ui()

        # Set up autosave timer
        self.autosave_interval = 60000  # 60 seconds
        self.autosave_timer = QTimer(self)
        self.autosave_timer.timeout.connect(self.autosave_data)
        self.autosave_timer.start(self.autosave_interval)
        self.showMaximized()

    def init_ui(self):
        """
        Initialize the user interface of the main window.
        This method sets up the main layout, including a splitter, tab widgets, 
        and various tabs for data editing, data output, and other functionalities. 
        It also configures the menu bar with different menus and actions, 
        and sets up the toolbar with various actions and icons.
        Tabs:
            - Data Editor: Allows editing of data in a spreadsheet format.
            - Data Output: Displays output data in a table view.
            - Output: Displays output in a scrollable area.
        Menus:
            - File: Contains actions to load and save files.
            - Exploration: Contains actions for data exploration such as summary data, normality test, correlation, etc.
            - Graph: Contains actions to generate different types of plots.
            - Model: Contains actions related to different modeling techniques.
            - Compute: Contains actions to compute new variables.
            - About: Contains information about the application.
            - Settings: Contains actions to change application settings.
        Toolbar:
            - Load File: Action to load a file.
            - Save Data: Action to save data.
            - Undo: Action to undo the last operation.
            - Redo: Action to redo the last undone operation.
            - Compute New Variable: Action to compute a new variable.
            - Setting: Action to open settings.
        Shortcuts:
            - Go to Start Row: Ctrl + Up
            - Go to End Row: Ctrl + Down
            - Go to Start Column: Ctrl + Left
            - Go to End Column: Ctrl + Right
        Layout:
            - The main layout is a vertical box layout containing the tab widget.
            - The central widget is set with this layout.
            - The main window is resized to a default size of 800x600.
        """
        
        # Membuat splitter utama untuk membagi halaman menjadi dua bagian (kiri dan kanan)
        self.splitter_main = QSplitter(Qt.Orientation.Horizontal, self)

        # Bagian kiri: QTabWidget untuk dua sheet
        self.tab_widget = QTabWidget(self.splitter_main)  # Ditambahkan ke splitter utama
        self.tab_widget.setTabPosition(QTabWidget.TabPosition.South)
        
        self.show_modeling_sae_dialog = None
        self.show_modeling_saeHB_dialog = None
        self.show_modeling_sae_unit_dialog = None
        self.show_modeling_saeHB_normal_dialog = None
        self.show_modellig_sae_pseudo_dialog = None
        self.show_compute_variable_dialog = None
        self.show_projection_variabel_dialog = None
        

        # Tab pertama (Data Editor)
        self.tab1 = QWidget()
        self.spreadsheet = QTableView(self.tab1)
        self.spreadsheet.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.spreadsheet.customContextMenuRequested.connect(lambda pos: show_context_menu(self, pos))
        self.spreadsheet.setModel(self.model1)
        self.spreadsheet.horizontalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.spreadsheet.horizontalHeader().customContextMenuRequested.connect(self.show_header_context_menu)
        self.spreadsheet.horizontalHeader().sectionDoubleClicked.connect(self.rename_column)
        self.spreadsheet.verticalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.spreadsheet.verticalHeader().customContextMenuRequested.connect(lambda pos: show_context_menu(self, pos))

        tab1_layout = QVBoxLayout(self.tab1)
        tab1_layout.addWidget(self.spreadsheet)
        self.spreadsheet.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # Tab kedua (Data Output)
        self.tab2 = QWidget()
        self.table_view2 = QTableView(self.tab2)
        self.table_view2.setModel(self.model2)
        self.table_view2.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)

        tab2_layout = QVBoxLayout(self.tab2)
        tab2_layout.addWidget(self.table_view2)

        # Tab ketiga (Output)
        self.tab3 = QWidget()
        self.scroll_area = QScrollArea(self.tab3)
        
        # Tab output
        self.output_tab = QWidget()
        self.scroll_area = QScrollArea(self.output_tab)
        self.scroll_area.setWidgetResizable(True)
        self.output_container = QWidget()
        self.output_layout = QVBoxLayout(self.output_container)
        self.scroll_area.verticalScrollBar().rangeChanged.connect(lambda: self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().maximum()))
        self.output_container.setLayout(self.output_layout)
        self.scroll_area.setWidget(self.output_container)
        
        # Atur layout output
        self.output_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.output_layout.setSpacing(0)

        tab3_layout = QVBoxLayout(self.tab3)
        tab3_layout.addWidget(self.scroll_area)

        # Menambahkan tab ke QTabWidget
        self.tab_widget.addTab(self.tab1, "Data Editor")
        self.tab_widget.addTab(self.tab2, "Data Output")
        self.tab_widget.addTab(self.tab3, "Output")  # Tab baru untuk output

        # Membuat layout utama
        layout = QVBoxLayout()
        layout.addWidget(self.tab_widget)
        

        # Widget utama dan layout
        central_widget = QWidget(self)
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

        # Membuat menu bar
        self.menu_bar = self.menuBar()

        # Membuat menu File -> Load dan Save
        self.file_menu = self.menu_bar.addMenu("File")
        
        self.recent_data = QAction("Open Recent data", self)
        self.recent_data.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_D))
        self.recent_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'recentdata.svg')))
        self.recent_data.setStatusTip("Ctrl+D")
        
        self.load_action = QAction("Load File", self)
        self.load_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_O))
        self.load_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'open.svg')))
        self.load_action.setStatusTip("Ctrl+O")
        
        self.load_secondary_data = QAction("Load File for Secondary Data", self)
        self.load_secondary_data.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_2))
        self.load_secondary_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'secondary.svg')))
        self.load_secondary_data.setStatusTip("Ctrl+2")
        
        self.save_action = QAction("Save Data", self)
        self.save_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_S))
        self.save_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savedata.svg')))
        self.save_action.setStatusTip("Ctrl+S")
        
        self.save_data_output_action = QAction("Save Data Output", self)
        self.save_data_output_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_S))
        self.save_data_output_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savedataoutput.svg')))
        self.save_action.setStatusTip("Ctrl+Shift+S")
        
        self.save_output_pdf = QAction("Save Output to PDF", self)
        self.save_output_pdf.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_P))
        self.save_output_pdf.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savepdf.svg')))
        self.save_output_pdf.setStatusTip("Ctrl+P")

        self.file_menu.addAction(self.recent_data)
        self.file_menu.addAction(self.load_action)
        self.file_menu.addAction(self.load_secondary_data)
        self.file_menu.addAction(self.save_action)
        self.file_menu.addAction(self.save_data_output_action)
        self.file_menu.addAction(self.save_output_pdf)

        # Menu "Exploration"
        self.menu_exploration = self.menu_bar.addMenu("Pre-Modeling")

        self.action_summary_data = QAction("Data Summary", self)
        self.action_summary_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'summary.svg')))
        self.action_summary_data.triggered.connect(self.open_summary_data_dialog_lazy)

        self.action_normality_test = QAction("Normality Test", self)
        self.action_normality_test.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'normality.svg')))
        self.action_normality_test.triggered.connect(self.open_normality_test_dialog_lazy)

        self.action_correlation = QAction("Correlation", self)
        self.action_correlation.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'correlation.svg')))
        self.action_correlation.triggered.connect(self.open_correlation_matrix_dialog_lazy)

        self.action_multicollinearity = QAction("Multicollinearity", self)
        self.action_multicollinearity.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'multicolinierity.svg')))
        self.action_multicollinearity.triggered.connect(self.open_multicollinearity_dialog_lazy)

        self.action_variable_selection = QAction("Variable Selection", self)
        self.action_variable_selection.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'varselection.svg')))
        self.action_variable_selection.triggered.connect(self.open_variable_selection_dialog_lazy)

        self.menu_exploration.addAction(self.action_summary_data)
        self.menu_exploration.addAction(self.action_normality_test)
        self.menu_exploration.addAction(self.action_correlation)
        self.menu_exploration.addAction(self.action_multicollinearity)
        self.menu_exploration.addAction(self.action_variable_selection)

        # Menu "Graph"
        self.menu_graph = self.menu_bar.addMenu("Graph")

        self.action_scatter_plot = QAction("Scatter Plot", self)
        self.action_scatter_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'scatterplot.svg')))
        self.action_scatter_plot.triggered.connect(self.open_scatter_plot_dialog_lazy)

        self.action_box_plot = QAction("Box Plot", self)
        self.action_box_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'boxplot.svg')))
        self.action_box_plot.triggered.connect(self.open_box_plot_dialog_lazy)

        self.action_line_plot = QAction("Line Plot", self)
        self.action_line_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'lineplot.svg')))
        self.action_line_plot.triggered.connect(self.open_line_plot_dialog_lazy)

        self.action_histogram = QAction("Histogram", self)
        self.action_histogram.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'histogram.svg')))
        self.action_histogram.triggered.connect(self.open_histogram_dialog_lazy)

        # Menambahkan plot-plot ke menu Graph
        self.menu_graph.addAction(self.action_scatter_plot)
        self.menu_graph.addAction(self.action_box_plot)
        self.menu_graph.addAction(self.action_line_plot)
        self.menu_graph.addAction(self.action_histogram)
        # Menu "Model"
        menu_model = self.menu_bar.addMenu("Model")

        # Submenu "Area Level"
        menu_area_level = QMenu("Area Level", self)
        menu_area_level.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'arealevel.svg')))
        action_eblup_area = QAction("EBLUP", self)
        action_eblup_area.triggered.connect(self.show_modeling_sae_dialog_lazy)
        action_hb_beta = QAction("HB Beta", self)
        action_hb_beta.triggered.connect(self.show_modeling_saeHB_dialog_lazy)
        menu_area_level.addAction(action_eblup_area)
        menu_area_level.addAction(action_hb_beta)

        # Submenu "Unit Level"
        menu_unit_level = QMenu("Unit Level", self)
        menu_unit_level.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'unitlevel.svg')))
        action_eblup_unit = QAction("EBLUP", self)
        action_eblup_unit.triggered.connect(self.show_modeling_sae_unit_dialog_lazy)
        action_hb_normal = QAction("HB Normal", self)
        action_hb_normal.triggered.connect(self.show_modeling_saeHB_normal_dialog_lazy)
        menu_unit_level.addAction(action_eblup_unit)
        menu_unit_level.addAction(action_hb_normal)

        # Submenu "Pseudo"
        menu_pseudo = QMenu("Pseudo", self)
        menu_pseudo.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'pseudo.svg')))
        action_eblup_pseudo = QAction("EBLUP", self)
        action_eblup_pseudo.triggered.connect(self.show_modellig_sae_pseudo_dialog_lazy)
        menu_pseudo.addAction(action_eblup_pseudo)

        # Submenu "Projection"
        menu_projection = QMenu("Projection", self)
        menu_projection.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'projection.svg')))
        action_projection = QAction("Projection", self)
        action_projection.triggered.connect(self.show_projection_variabel_dialog_lazy)
        menu_projection.addAction(action_projection)


        # Menambahkan submenu ke menu "Model"
        menu_model.addMenu(menu_area_level)
        menu_model.addMenu(menu_unit_level)
        menu_model.addMenu(menu_pseudo)
        menu_model.addMenu(menu_projection)



         # Menu 'Compute'
        menu_compute = self.menu_bar.addMenu("Compute")
        compute_new_var = QAction("Compute New Variable", self)
        compute_new_var.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'compute.svg')))
        compute_new_var.triggered.connect(self.show_compute_variable_dialog_lazy)
        menu_compute.addAction(compute_new_var)
        
        # Menu "About"
        menu_about = self.menu_bar.addMenu("About")
        action_about_info = QAction("About This App", self)
        action_about_info.triggered.connect(self.open_about_dialog)
        action_about_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'about.svg')))
        menu_about.addAction(action_about_info)
        
        action_header_icon_info = QAction("Header Icon Info", self)
        action_header_icon_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'information.svg')))
        action_header_icon_info.triggered.connect(self.show_header_icon_info)
        menu_about.addAction(action_header_icon_info)
        
        # Add R Packages Used menu
        action_r_packages_info = QAction("R Packages Used", self)
        action_r_packages_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'Rinfo.svg')))
        action_r_packages_info.triggered.connect(self.show_r_packages_info)
        menu_about.addAction(action_r_packages_info)
        

        # Tool Bar
        self.toolBar = QToolBar(self)
        self.toolBar.setIconSize(QSize(45, 35))
        self.toolBar.setObjectName("toolBar")
        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolBar)  # Perbaikan dilakukan di sini
        # Actions for Toolbar
        self.actionLoad_file = QAction(self)  # Menggunakan self untuk referensi instance
        icon_load = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'open.svg'))
        self.actionLoad_file.setIcon(icon_load)
        self.actionLoad_file.setText("Load File")
        self.toolBar.addAction(self.actionLoad_file)

        self.actionSave_Data = QAction(self)  # Menggunakan self untuk referensi instance
        icon_save = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'save.svg'))
        self.actionSave_Data.setIcon(icon_save)
        self.actionSave_Data.setText("Save Data")
        self.toolBar.addAction(self.actionSave_Data)

        self.actionUndo = QAction(self)
        icon_undo = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'undo.svg'))
        self.actionUndo.setIcon(icon_undo)
        self.actionUndo.setText("Undo")
        self.actionUndo.triggered.connect(self.undo_action)
        self.toolBar.addAction(self.actionUndo)

        self.actionRedo = QAction(self)
        icon_redo = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'redo.svg'))
        self.actionRedo.setIcon(icon_redo)
        self.actionRedo.setText("Redo")
        self.actionRedo.triggered.connect(self.redo_action)
        self.toolBar.addAction(self.actionRedo)
        
        self.actionCompute = QAction(self)
        icon_compute = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'compute.svg'))
        self.actionCompute.setIcon(icon_compute)
        self.actionCompute.setText("Compute New Variable")
        self.actionCompute.triggered.connect(self.show_compute_variable_dialog_lazy)
        self.toolBar.addAction(self.actionCompute)
        
        # Shortcuts for "Go to Start/End Row/Column"
        self.go_to_start_row_action = QAction(self)
        self.go_to_start_row_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Up))
        self.go_to_start_row_action.triggered.connect(lambda : go_to_start_row(self))
        self.addAction(self.go_to_start_row_action)

        self.go_to_end_row_action = QAction(self)
        self.go_to_end_row_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Down))
        self.go_to_end_row_action.triggered.connect(lambda : go_to_end_row(self))
        self.addAction(self.go_to_end_row_action)

        self.go_to_start_column_action = QAction(self)
        self.go_to_start_column_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Left))
        self.go_to_start_column_action.triggered.connect(lambda : go_to_start_column(self))
        self.addAction(self.go_to_start_column_action)

        self.go_to_end_column_action = QAction(self)
        self.go_to_end_column_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Right))
        self.go_to_end_column_action.triggered.connect(lambda : go_to_end_column(self))
        self.addAction(self.go_to_end_column_action)

        # Add spacer to push following items to the right
        spacer = QWidget(self)
        spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.toolBar.addWidget(spacer)

        # Add "Setting" button to the right
        self.actionSetting = QAction(self)
        icon_setting = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'setting.svg'))
        self.actionSetting.setIcon(icon_setting)
        self.actionSetting.setText("Setting")
        self.actionSetting.triggered.connect(self.change_font_size)
        self.toolBar.addAction(self.actionSetting)
        

        # Menu "Settings"
        menu_settings = self.menu_bar.addMenu("Settings")
        action_change_font_size = QAction("Change Font Size", self)
        action_change_font_size.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'setting.svg')))
        action_change_font_size.triggered.connect(self.change_font_size)
        menu_settings.addAction(action_change_font_size)

        # Menetapkan ukuran default
        self.resize(800, 600)

    
    def change_font_size(self):
        """
        Opens a dialog to change the font size of the application.
        The dialog presents three font size options: "Small", "Medium", and "Big".
        The user can select a font size from a combo box, and the selected size
        will be applied to the application if the user confirms the selection.
        The dialog also displays a sample text ("AaBbCc") that updates in real-time
        to reflect the selected font size.
        Attributes:
            sizes (dict): A dictionary mapping font size names to their corresponding pixel values.
            items (list): A list of font size names.
            dialog (QDialog): The dialog window for selecting the font size.
            layout (QVBoxLayout): The main layout of the dialog.
            combo_box (QComboBox): The combo box for selecting the font size.
            display (QLabel): The label displaying the sample text with the selected font size.
            button_box (QHBoxLayout): The layout containing the OK and Cancel buttons.
            ok_button (QPushButton): The button to confirm the font size selection.
            cancel_button (QPushButton): The button to cancel the font size selection.
        Methods:
            update_display_font_size: Updates the sample text's font size based on the selected size in the combo box.
        """
        
        sizes = {"Small": 10, "Medium": 12, "Big": 16}
        items = list(sizes.keys())

        dialog = QDialog(self)
        dialog.setWindowTitle("Select Font Size")
        dialog.setMinimumWidth(200)

        layout = QVBoxLayout(dialog)

        combo_box = QComboBox()
        combo_box.addItems(items)
        default_size = next(key for key, value in sizes.items() if value == self.font_size)
        combo_box.setCurrentText(default_size)
        layout.addWidget(combo_box)
        
        display = QLabel("AaBbCc")
        display.setAlignment(Qt.AlignmentFlag.AlignCenter)
        display.setStyleSheet(f"font-size: {self.font_size}px;")
        layout.addWidget(display)
    
        def update_display_font_size():
            size = combo_box.currentText()
            display.setStyleSheet(f"font-size: {sizes[size]}px;")

        combo_box.currentTextChanged.connect(update_display_font_size)

        button_box = QHBoxLayout()
        ok_button = QPushButton("OK")
        cancel_button = QPushButton("Cancel")
        button_box.addWidget(ok_button)
        button_box.addWidget(cancel_button)
        layout.addLayout(button_box)

        ok_button.clicked.connect(dialog.accept)
        cancel_button.clicked.connect(dialog.reject)

        dialog.setLayout(layout)

        if dialog.exec() == QDialog.DialogCode.Accepted:
            selected_size = combo_box.currentText()
            self.set_font_size(sizes[selected_size])
            self.font_size = sizes[selected_size]

    def set_font_size(self, size):
        """
        Sets the font size for the main window.
        Args:
            size (int): The desired font size to be set.
        This method updates the stylesheet of the main window with the specified font size.
        """
        
        stylesheet = self.load_stylesheet_with_font_size(size)
        self.setStyleSheet(stylesheet)

    def load_stylesheet_with_font_size(self, size):
        """
        Memuat stylesheet dan mengganti ukuran font global.
        """
        stylesheet_path = os.path.join(self.path, 'assets', 'style', 'style.qss')
        if os.path.exists(stylesheet_path):
            with open(stylesheet_path, 'r') as file:
                stylesheet = file.read()
                # stylesheet = stylesheet.replace("font-size: 12px;", f"font-size: {size}px;")
                stylesheet = stylesheet.replace("__FONT_SIZE__", f"{size}px")
                return stylesheet
        else:
            print(f"Stylesheet tidak ditemukan di {stylesheet_path}")
            return ""
    
    def open_summary_data_dialog_lazy(self):
        """
        Lazily initializes and opens the summary data dialog.
        This method checks if the 'show_summary_data_dialog' attribute exists.
        If it does not exist, it initializes it with an instance of SummaryDataDialog.
        Then, it calls the 'open_summary_data_dialog' method to open the dialog.
        """
        
        if not hasattr(self, 'show_summary_data_dialog'):
            self.show_summary_data_dialog = SummaryDataDialog(self)
        self.open_summary_data_dialog()

    def open_normality_test_dialog_lazy(self):
        """
        Lazily initializes and opens the normality test dialog.
        This method checks if the normality test dialog has already been created.
        If not, it initializes the dialog and then opens it. If the dialog has 
        already been created, it simply opens the existing instance.
        """
        
        if not hasattr(self, 'show_normality_test_dialog'):
            self.show_normality_test_dialog = NormalityTestDialog(self)
        self.open_normality_test_dialog()

    def open_multicollinearity_dialog_lazy(self):
        """
        Lazily initializes and opens the multicollinearity dialog.
        This method checks if the 'show_multicollinearity_dialog' attribute exists.
        If it does not, it initializes it with an instance of MulticollinearityDialog.
        Then, it opens the multicollinearity dialog.
        Returns:
            None
        """
        
        if not hasattr(self, 'show_multicollinearity_dialog'):
            self.show_multicollinearity_dialog = MulticollinearityDialog(self)
        self.open_multicollinearity_dialog()

    def open_variable_selection_dialog_lazy(self):
        """
        Lazily initializes and opens the variable selection dialog.
        This method checks if the 'show_variable_selection_dialog' attribute
        exists. If it does not, it initializes it with an instance of 
        VariableSelectionDialog. Then, it calls the method to open the 
        variable selection dialog.
        """
        
        if not hasattr(self, 'show_variable_selection_dialog'):
            self.show_variable_selection_dialog = VariableSelectionDialog(self)
        self.open_variable_selection_dialog()
    
    def open_scatter_plot_dialog_lazy(self):
        """
        Lazily initializes and opens the scatter plot dialog.
        This method checks if the scatter plot dialog has already been created.
        If not, it initializes the dialog and then opens it. This ensures that
        the dialog is only created when needed, potentially saving resources.
        Attributes:
            show_scatter_plot_dialog (ScatterPlotDialog): The scatter plot dialog instance.
        """
        
        if not hasattr(self, 'show_scatter_plot_dialog'):
            self.show_scatter_plot_dialog = ScatterPlotDialog(self)
        self.open_scatter_plot_dialog()

    def open_correlation_matrix_dialog_lazy(self):
        """
        Lazily initializes and opens the correlation matrix dialog.
        This method checks if the correlation matrix dialog has already been created.
        If not, it initializes the dialog. Then, it opens the dialog.
        Attributes:
            show_correlation_matrix_dialog (CorrelationMatrixDialog): The correlation matrix dialog instance.
        """
        
        if not hasattr(self, 'show_correlation_matrix_dialog'):
            self.show_correlation_matrix_dialog = CorrelationMatrixDialog(self)
        self.open_correlation_matrix_dialog()

    def open_box_plot_dialog_lazy(self):
        """
        Lazily initializes and opens the box plot dialog.
        This method checks if the 'show_box_plot_dialog' attribute exists.
        If it does not, it initializes 'show_box_plot_dialog' with an instance
        of BoxPlotDialog. Then, it calls the method to open the box plot dialog.
        """
        
        if not hasattr(self, 'show_box_plot_dialog'):
            self.show_box_plot_dialog = BoxPlotDialog(self)
        self.open_box_plot_dialog()

    def open_line_plot_dialog_lazy(self):
        """
        Lazily initializes and opens the line plot dialog.
        This method checks if the 'show_line_plot_dialog' attribute exists.
        If it does not, it initializes 'show_line_plot_dialog' with an instance
        of LinePlotDialog. Then, it calls the method to open the line plot dialog.
        """
        
        if not hasattr(self, 'show_line_plot_dialog'):
            self.show_line_plot_dialog = LinePlotDialog(self)
        self.open_line_plot_dialog()

    def open_histogram_dialog_lazy(self):
        """
        Opens the histogram dialog lazily.
        This method checks if the histogram dialog has already been created.
        If not, it initializes the HistogramDialog and assigns it to the 
        'show_histogram_dialog' attribute. Then, it opens the histogram dialog.
        """
        
        if not hasattr(self, 'show_histogram_dialog'):
            self.show_histogram_dialog = HistogramDialog(self)
        self.open_histogram_dialog()
    
    def open_summary_data_dialog(self):
        """
        Opens the summary data dialog.
        This method sets the models for the summary data dialog and then displays the dialog.
        It uses `self.model1` and `self.model2` as the models to be set in the dialog.
        Returns:
            None
        """
        
        self.show_summary_data_dialog.set_model(self.model1, self.model2)
        self.show_summary_data_dialog.show()
        
    def open_normality_test_dialog(self):
        """
        Opens the normality test dialog.
        This method sets the models for the normality test dialog and then displays the dialog.
        It uses `self.model1` and `self.model2` as the models to be set in the dialog.
        Returns:
            None
        """
        
        self.show_normality_test_dialog.set_model( self.model1, self.model2)
        self.show_normality_test_dialog.show()

    def open_scatter_plot_dialog(self):
        """
        Opens the scatter plot dialog and sets the models for the dialog.
        This method sets the models for the scatter plot dialog using 
        `self.model1` and `self.model2`, and then displays the dialog.
        """
        
        self.show_scatter_plot_dialog.set_model(self.model1, self.model2)
        self.show_scatter_plot_dialog.show()

    def open_line_plot_dialog(self):
        """
        Opens the line plot dialog and sets the models for the dialog.
        This method sets the models for the line plot dialog using `self.model1` 
        and `self.model2`, and then displays the dialog.
        Returns:
            None
        """
        
        self.show_line_plot_dialog.set_model(self.model1, self.model2)
        self.show_line_plot_dialog.show()
    
    def open_box_plot_dialog(self):
        """
        Opens the box plot dialog and sets the models for the dialog.
        This method sets the models for the box plot dialog using `self.model1` and `self.model2`,
        and then displays the dialog.
        Returns:
            None
        """
        
        self.show_box_plot_dialog.set_model(self.model1, self.model2)
        self.show_box_plot_dialog.show()

    def open_correlation_matrix_dialog(self):
        """
        Opens the correlation matrix dialog.
        This method sets the model data for the correlation matrix dialog
        and then displays the dialog to the user.
        The models used are `self.model1` and `self.model2`.
        """
        
        self.show_correlation_matrix_dialog.set_model(self.model1, self.model2)
        self.show_correlation_matrix_dialog.show()
    
    def open_multicollinearity_dialog(self):
        """
        Opens the multicollinearity dialog and sets the models for it.
        This method sets the models for the multicollinearity dialog using
        `self.model1` and `self.model2`, and then displays the dialog.
        """
        
        self.show_multicollinearity_dialog.set_model(self.model1, self.model2)
        self.show_multicollinearity_dialog.show()
    
    def open_histogram_dialog(self):
        """
        Opens the histogram dialog and sets the models for it.
        This method sets the models for the histogram dialog using `self.model1` and `self.model2`,
        and then displays the histogram dialog.
        """
        
        self.show_histogram_dialog.set_model(self.model1, self.model2)
        self.show_histogram_dialog.show()

    def open_variable_selection_dialog(self):
        """
        Opens the variable selection dialog.
        This method sets the model for the variable selection dialog using 
        `self.model1` and `self.model2`, and then displays the dialog.
        """
        
        self.show_variable_selection_dialog.set_model(self.model1, self.model2)
        self.show_variable_selection_dialog.show()

    def show_modeling_sae_dialog_lazy(self):
        """
        Lazily initializes and displays the ModelingSaeDialog.
        If the ModelingSaeDialog has not been created yet, this method will
        instantiate it and set its model to `self.model1`. Then, it will
        display the dialog.
        Returns:
            None
        """
        
        if self.show_modeling_sae_dialog is None:
            self.show_modeling_sae_dialog = ModelingSaeDialog(self)
        self.show_modeling_sae_dialog.set_model(self.model1)
        self.show_modeling_sae_dialog.show()

    def show_modeling_saeHB_dialog_lazy(self):
        """
        Displays the ModelingSaeHBDialog lazily.
        This method initializes the ModelingSaeHBDialog if it has not been created yet,
        sets its model to `self.model1`, and then shows the dialog.
        Attributes:
            show_modeling_saeHB_dialog (ModelingSaeHBDialog): The dialog instance to be shown.
            model1: The model to be set in the dialog.
        """
        
        if self.show_modeling_saeHB_dialog is None:
            self.show_modeling_saeHB_dialog = ModelingSaeHBDialog(self)
        self.show_modeling_saeHB_dialog.set_model(self.model1)
        self.show_modeling_saeHB_dialog.show()

    def show_modeling_sae_unit_dialog_lazy(self):
        """
        Lazily initializes and displays the ModelingSaeUnitDialog.
        If the dialog has not been created yet, it initializes it with the current instance.
        Then, it sets the model for the dialog and shows it.
        Attributes:
            show_modeling_sae_unit_dialog (ModelingSaeUnitDialog): The dialog for modeling SAE units.
            model1: The model to be set in the dialog.
        """
        
        if self.show_modeling_sae_unit_dialog is None:
            self.show_modeling_sae_unit_dialog = ModelingSaeUnitDialog(self)
        self.show_modeling_sae_unit_dialog.set_model(self.model1)
        self.show_modeling_sae_unit_dialog.show()

    def show_modeling_saeHB_normal_dialog_lazy(self):
        """
        Lazily initializes and displays the ModelingSaeHBNormalDialog.
        If the dialog has not been created yet, it initializes a new instance
        of ModelingSaeHBNormalDialog and sets its model to self.model1. 
        Then, it shows the dialog.
        Attributes:
            show_modeling_saeHB_normal_dialog (ModelingSaeHBNormalDialog): 
            The dialog instance to be displayed.
            model1: The model to be set in the dialog.
        """
        
        if self.show_modeling_saeHB_normal_dialog is None:
            self.show_modeling_saeHB_normal_dialog = ModelingSaeHBNormalDialog(self)
        self.show_modeling_saeHB_normal_dialog.set_model(self.model1)
        self.show_modeling_saeHB_normal_dialog.show()

    def show_modellig_sae_pseudo_dialog_lazy(self):
        """
        Lazily initializes and displays the Modeling SAE Pseudo dialog.
        This method checks if the `show_modellig_sae_pseudo_dialog` attribute is None.
        If it is, it initializes a new instance of `ModelingSaePseudoDialog` with the current
        instance (`self`) as the parent. It then sets the model for the dialog using `self.model1`
        and displays the dialog.
        Returns:
            None
        """
        
        if self.show_modellig_sae_pseudo_dialog is None:
            self.show_modellig_sae_pseudo_dialog = ModelingSaePseudoDialog(self)
        self.show_modellig_sae_pseudo_dialog.set_model(self.model1)
        self.show_modellig_sae_pseudo_dialog.show()

    def show_compute_variable_dialog_lazy(self):
        """
        Displays the Compute Variable dialog lazily.
        This method initializes the Compute Variable dialog if it hasn't been created yet,
        sets its model to `self.model1`, and then shows the dialog.
        If `self.show_compute_variable_dialog` is already initialized, it simply updates
        the model and shows the dialog.
        Returns:
            None
        """
        
        if self.show_compute_variable_dialog is None:
            self.show_compute_variable_dialog = ComputeVariableDialog(self)
        self.show_compute_variable_dialog.set_model(self.model1)
        self.show_compute_variable_dialog.show()

    def show_projection_variabel_dialog_lazy(self):
        """
        Lazily initializes and displays the ProjectionDialog.
        This method checks if the `show_projection_variabel_dialog` attribute is None.
        If it is, it initializes it with a new instance of `ProjectionDialog`.
        It then sets the model for the dialog and checks if the prerequisites for
        showing the dialog are met. If they are, it displays the dialog.
        Attributes:
            show_projection_variabel_dialog (ProjectionDialog): The dialog to be shown.
            model1: The model to be set in the dialog.
        Returns:
            None
        """
        
        if self.show_projection_variabel_dialog is None:
            self.show_projection_variabel_dialog = ProjectionDialog(self)
        self.show_projection_variabel_dialog.set_model(self.model1)
        if self.show_projection_variabel_dialog.show_prerequisites():
            self.show_projection_variabel_dialog.show()

    def open_about_dialog(self):
        """
        Opens the About dialog window.
        This method creates an instance of the AboutDialog class, passing the
        current instance as the parent, and then executes the dialog.
        """
        
        about_dialog = AboutDialog(self)
        about_dialog.exec()

    def add_row(self, sheet_number):
        """Sinkronisasi data ketika baris baru ditambahkan di SpreadsheetWidget."""
        if sheet_number == 1:
            # Tambahkan baris baru di DataFrame data1
            new_row = pl.DataFrame({col: [""] for col in self.data1.columns})
            self.data1 = pl.concat([self.data1, new_row])
        elif sheet_number == 2:
            pass  # Tidak digunakan untuk Sheet 2
    
    def add_column(self, sheet_number):
        """Sinkronisasi data ketika kolom baru ditambahkan di SpreadsheetWidget."""
        if sheet_number == 1:
            # Tambahkan kolom baru di DataFrame data1
            new_column_name = f"Column {self.data1.shape[1] + 1}"
            new_column = pl.DataFrame({new_column_name: [""] * self.data1.shape[0]})
            self.data1 = pl.concat([self.data1, new_column], how="horizontal")
        elif sheet_number == 2:
            pass  # Tidak digunakan untuk Sheet 2
    
    def update_table(self, sheet_number, model):
        """Memperbarui tabel pada sheet tertentu dengan model baru"""
        if sheet_number == 1:
            self.spreadsheet.setModel(model)
            self.model1 = model
            self.spreadsheet.resizeColumnsToContents()
            self.tab_widget.setCurrentWidget(self.tab1)
            if self.show_modeling_sae_dialog:
                self.show_modeling_sae_dialog.set_model(model)
            if self.show_modeling_saeHB_dialog:
                self.show_modeling_saeHB_dialog.set_model(model)
            if self.show_modeling_sae_unit_dialog:
                self.show_modeling_sae_unit_dialog.set_model(model)
            if self.show_modeling_saeHB_normal_dialog:
                self.show_modeling_saeHB_normal_dialog.set_model(model)
            if self.show_modellig_sae_pseudo_dialog:
                self.show_modellig_sae_pseudo_dialog.set_model(model)
            if self.show_compute_variable_dialog:
                self.show_compute_variable_dialog.set_model(model)
            if self.show_projection_variabel_dialog:
                self.show_projection_variabel_dialog.set_model(model)
        elif sheet_number == 2:
            self.table_view2.setModel(model)
            self.model2 = model
            self.table_view2.resizeColumnsToContents()

    def keyPressEvent(self, event):
        """Handle keyboard shortcuts for copy, paste, undo, and redo."""
        if event.matches(QKeySequence.StandardKey.Copy):
            self.copy_selection()
        elif event.matches(QKeySequence.StandardKey.Paste):
            self.paste_selection()
        elif event.matches(QKeySequence.StandardKey.Undo):
            self.undo_action()
        elif event.matches(QKeySequence.StandardKey.Redo):
            self.redo_action()
        else:
            super().keyPressEvent(event)

    def copy_selection(self):
        """Copy selected cells to clipboard."""
        selection = self.spreadsheet.selectionModel().selectedIndexes()
        if selection:
            data = '\n'.join(['\t'.join([self.model1.data(index, Qt.ItemDataRole.DisplayRole) for index in row]) for row in self.group_by_row(selection)])
            clipboard = QApplication.clipboard()
            clipboard.setText(data)

    def paste_selection(self):
        """Paste clipboard content to selected cells."""
        clipboard = QApplication.clipboard()
        data = clipboard.text().split('\n')
        selection = self.spreadsheet.selectionModel().selectedIndexes()
        if selection:
            start_row = selection[0].row()
            start_col = selection[0].column()
            for i, row in enumerate(data):
                for j, value in enumerate(row.split('\t')):
                    index = self.model1.index(start_row + i, start_col + j)
                    self.model1.setData(index, value, Qt.ItemDataRole.EditRole)

    def undo_action(self):
        """Undo the last action."""
        self.model1.undo()

    def redo_action(self):
        """Redo the last undone action."""
        self.model1.redo()

    def group_by_row(self, selection):
        """Group selected indexes by row."""
        rows = {}
        for index in selection:
            if index.row() not in rows:
                rows[index.row()] = []
            rows[index.row()].append(index)
        return [rows[row] for row in sorted(rows)]
    
    def show_output(self, title, content):
        """Display output in the Output tab"""
        label = QLabel(content)
        self.output_layout.addWidget(label)
        self.output_tab_widget.setCurrentIndex(0)

    def show_header_context_menu(self, pos):
        """Show context menu for header."""
        header = self.spreadsheet.horizontalHeader()
        logical_index = header.logicalIndexAt(pos)
        menu = QMenu(self)
        rename_action = QAction("Rename Column", self)
        rename_action.triggered.connect(lambda: self.rename_column(logical_index))
        menu.addAction(rename_action)
        
        edit_type_action = QAction("Edit Data Type", self)
        edit_type_action.triggered.connect(lambda: self.edit_data_type(logical_index))
        menu.addAction(edit_type_action)
        
        selection = self.spreadsheet.selectionModel().selectedIndexes()
        has_selection = bool(selection)
        
        delete_column_action = QAction("Delete Column", self)
        delete_column_action.triggered.connect(lambda : confirm_delete_selected_columns(self))
        delete_column_action.setEnabled(has_selection)
        menu.addAction(delete_column_action)
        
        add_column_before_action = QAction("Add Column Before", self)
        add_column_before_action.triggered.connect(lambda: show_add_column_before_dialog(self))
        add_column_before_action.setEnabled(has_selection)
        menu.addAction(add_column_before_action)
        
        add_column_after_action = QAction("Add Column After", self)
        add_column_after_action.triggered.connect(lambda: show_add_column_after_dialog(self))
        add_column_after_action.setEnabled(has_selection)
        menu.addAction(add_column_after_action)
        
        menu.exec(header.mapToGlobal(pos))

    def rename_column(self, column_index):
        """Rename the column at the given index."""
        current_name = self.model1.headerData(column_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole)
        new_name, ok = QInputDialog.getText(self, "Rename Column", "New column name:", text=current_name)
        if ok and new_name:
            self.model1.rename_column(column_index, new_name)
            self.update_table(1, self.model1)

    def edit_data_type(self, column_index):
        """Edit the data type of the column at the given index."""
        current_type = self.model1.get_column_type(column_index)
        if current_type==pl.Utf8:
            current_type = "String"
        elif current_type==pl.Int64:
            current_type = "Integer"
        elif current_type==pl.Float64:
            current_type = "Float"
        type_list = ["String", "Integer", "Float"]
        current_index = type_list.index(current_type)
        new_type, ok = QInputDialog.getItem(self, "Edit Data Type", "Select new data type:", type_list, current=current_index)
        if ok and new_type:
            self.model1.set_column_type(column_index, new_type)
            self.update_table(1, self.model1)
    
    def set_path(self, path):
        """
        Sets the path attribute for the MainWindow instance.
        Args:
            path (str): The path to be set.
        """
        
        self.path=path
    
    def add_output(self, script_text, result_text=None, plot_paths=None):
        """Menambahkan output ke layout dalam bentuk card"""

        card_frame = QFrame()
        card_frame.setStyleSheet("""
            QFrame {
                background-color: #f9f9f9;
                border: 1px solid #ddd;
                border-radius: 8px;
                padding: 10px;
            }
        """)

        card_layout = QVBoxLayout(card_frame)
        card_layout.setSpacing(8)

        label_script = QLabel("<b>Script R:</b>")
        label_script.setStyleSheet("color: #333; margin-bottom: 5px;")
        script_box = QTextEdit()
        script_box.setPlainText(script_text)
        script_box.setReadOnly(True)
        script_box.setStyleSheet("""
            QTextEdit {
                background-color: #fff;
                border: 1px solid #ccc;
                border-radius: 4px;
                padding: 5px;
                font-family: Consolas, Courier New, monospace;
            }
        """)
        script_box.setFixedHeight(script_box.fontMetrics().lineSpacing() * (script_text.count('\n') + 3))

        card_layout.addWidget(label_script)
        card_layout.addWidget(script_box)

        if result_text:
            label_output = QLabel("<b>Output:</b>")
            label_output.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
            result_box = QTextEdit()
            result_box.setPlainText(result_text)
            result_box.setReadOnly(True)
            result_box.setStyleSheet("""
                QTextEdit {
                    background-color: #fff;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    padding: 5px;
                    font-family: Consolas, Courier New, monospace;
                }
            """)
            max_height = 400
            calculated_height = result_box.fontMetrics().lineSpacing() * (result_text.count('\n') + 3)
            result_box.setFixedHeight(min(calculated_height, max_height))
            result_box.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn if calculated_height > max_height else Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

            card_layout.addWidget(label_output)
            card_layout.addWidget(result_box)

        if plot_paths:
            label_plot = QLabel("<b>Plot:</b>")
            label_plot.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
            card_layout.addWidget(label_plot)

            for plot_path in plot_paths:
                if os.path.exists(plot_path):
                    pixmap = QPixmap(plot_path)
                    label = QLabel()
                    label.setPixmap(pixmap)
                    label.setFixedSize(500, 350) 
                    label.setScaledContents(True)
                    label.setStyleSheet("border: 1px solid #ccc; border-radius: 4px;")
                    card_layout.addWidget(label)
        card_frame.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        card_frame.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos, card_frame))

        if self.output_layout.count() > 0:
            last_item = self.output_layout.itemAt(self.output_layout.count() - 1)
            if isinstance(last_item.spacerItem(), QSpacerItem):
                self.output_layout.removeItem(last_item)

        self.output_layout.addWidget(card_frame)

        if self.output_layout.count() == 1:
            self.output_layout.addStretch()

        self.tab_widget.setCurrentWidget(self.tab3)

        if plot_paths:
            for plot_path in plot_paths:
                if os.path.exists(plot_path):
                    os.remove(plot_path)


    def add_output(self, script_text, result_text=None, plot_paths=None, error_text=None):
        """Add output to the layout in the form of a card"""

        # Create a frame as a card
        card_frame = QFrame()
        card_frame.setStyleSheet("""
            QFrame {
                background-color: #f9f9f9;
                border: 1px solid #ddd;
                border-radius: 8px;
                padding: 10px;
            }
        """)


        card_layout = QVBoxLayout(card_frame)
        card_layout.setSpacing(8)

        label_script = QLabel("<b>R Script:</b>")
        label_script.setStyleSheet("color: #333; margin-bottom: 5px;")
        script_box = QTextEdit()
        script_box.setPlainText(script_text)
        script_box.setReadOnly(True)
        script_box.setStyleSheet("""
            QTextEdit {
                background-color: #fff;
                border: 1px solid #ccc;
                border-radius: 4px;
                padding: 5px;
                font-family: Consolas, Courier New, monospace;
            }
        """)
        script_box.setFixedHeight(script_box.fontMetrics().lineSpacing() * (script_text.count('\n') + 3))

        card_layout.addWidget(label_script)
        card_layout.addWidget(script_box)

        if result_text:
            label_output = QLabel("<b>Output:</b>")
            label_output.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
            result_box = QTextEdit()
            result_box.setPlainText(result_text)
            result_box.setReadOnly(True)
            result_box.setStyleSheet("""
                QTextEdit {
                    background-color: #fff;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    padding: 5px;
                    font-family: Consolas, Courier New, monospace;
                }
            """)
            max_height = 400
            calculated_height = result_box.fontMetrics().lineSpacing() * (result_text.count('\n') + 3)
            result_box.setFixedHeight(min(calculated_height, max_height))
            result_box.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn if calculated_height > max_height else Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

            card_layout.addWidget(label_output)
            card_layout.addWidget(result_box)

        if plot_paths:
            label_plot = QLabel("<b>Plot:</b>")
            label_plot.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
            card_layout.addWidget(label_plot)

            for plot_path in plot_paths:
                if os.path.exists(plot_path):
                    pixmap = QPixmap(plot_path)
                    label = QLabel()
                    label.setPixmap(pixmap)
                    label.setFixedSize(500, 350)
                    label.setScaledContents(True)
                    label.setStyleSheet("border: 1px solid #ccc; border-radius: 4px;")
                    card_layout.addWidget(label)

        if error_text:
            label_error = QLabel("<b>Error:</b>")
            label_error.setStyleSheet("color: #a94442; margin-top: 10px; margin-bottom: 5px;")
            error_box = QTextEdit()
            error_box.setPlainText(error_text)
            error_box.setReadOnly(True)
            error_box.setStyleSheet("""
                QTextEdit {
                    background-color: #f8d7da;
                    border: 1px solid #f5c6cb;
                    border-radius: 4px;
                    padding: 5px;
                    font-family: Consolas, Courier New, monospace;
                    color: #721c24;
                }
            """)
            error_box.setFixedHeight(error_box.fontMetrics().lineSpacing() * (error_text.count('\n') + 3))

            card_layout.addWidget(label_error)
            card_layout.addWidget(error_box)

        card_frame.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        card_frame.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos, card_frame))

        if self.output_layout.count() > 0:
            last_item = self.output_layout.itemAt(self.output_layout.count() - 1)
            if isinstance(last_item.spacerItem(), QSpacerItem):
                self.output_layout.removeItem(last_item)

        self.output_layout.addWidget(card_frame)
        if self.output_layout.count() == 1:
            self.output_layout.addStretch()

        self.tab_widget.setCurrentWidget(self.tab3)

        if plot_paths:
            for plot_path in plot_paths:
                if os.path.exists(plot_path):
                    os.remove(plot_path)

    def remove_output(self, card_frame):
        """Menghapus output dari layout"""
        self.output_layout.removeWidget(card_frame)
        card_frame.deleteLater()

        # Hapus spacer jika masih ada widget lain
        if self.output_layout.count() > 0:
            last_item = self.output_layout.itemAt(self.output_layout.count() - 1)
            if isinstance(last_item.spacerItem(), QSpacerItem):
                self.output_layout.removeItem(last_item)

        # Tambahkan stretch hanya jika tidak ada output tersisa
        if self.output_layout.count() == 0:
            self.output_layout.addStretch()

    def copy_output_image(self, card_frame):
        """Menyalin gambar output ke clipboard"""
        for child in card_frame.findChildren(QLabel):
            pixmap = child.pixmap()
            
            if pixmap and not pixmap.isNull():
                temp_folder = os.path.join(self.path, 'temp')
                temp_path = os.path.join(temp_folder, 'temp_image.png')

                os.makedirs(temp_folder, exist_ok=True)

                if pixmap.save(temp_path):
                    print(f"Gambar disimpan di: {temp_path}")
                    
                    clipboard = QApplication.clipboard()
                    clipboard.setPixmap(QPixmap(temp_path))

                    if os.path.exists(temp_path):  
                        os.remove(temp_path)
                break

    def show_context_menu(self, pos, card_frame):
        """Menampilkan menu klik kanan di setiap output"""
        menu = QMenu(self)
        delete_action = menu.addAction("Hapus Output")
        copy_image_action = menu.addAction("Copy Output Image")
        action = menu.exec(card_frame.mapToGlobal(pos))

        if action == delete_action:
            self.remove_output(card_frame)
        elif action == copy_image_action:
            self.copy_output_image(card_frame)

    def autosave_data(self):
        """
        Save the current state of data1, data2 (as parquet), and output (as JSON) to temporary files.
        """
        app_data_dir = os.path.join(os.getenv("APPDATA"), "saePisan")
        os.makedirs(app_data_dir, exist_ok=True)
        temp_dir = os.path.join(app_data_dir, 'file-data')
        os.makedirs(temp_dir, exist_ok=True)
        # Save data1 and data2 as parquet
        data1_path = os.path.join(temp_dir, 'sae_pisan_data1.parquet')
        data2_path = os.path.join(temp_dir, 'sae_pisan_data2.parquet')
        self.model1.get_data().write_parquet(data1_path)
        self.model2.get_data().write_parquet(data2_path)
        # Save output as JSON
        output_path = os.path.join(temp_dir, 'sae_pisan_output.json')
        import numpy as np

        def make_json_serializable(obj):
            if isinstance(obj, dict):
                return {k: make_json_serializable(v) for k, v in obj.items()}
            elif isinstance(obj, list):
                return [make_json_serializable(v) for v in obj]
            elif isinstance(obj, np.ndarray):
                return obj.tolist()
            elif hasattr(obj, 'tolist'):
                return obj.tolist()
            elif isinstance(obj, (int, float, str, type(None), bool)):
                return obj
            else:
                return str(obj)

        serializable_data = make_json_serializable(self.data)
        data = {
            'timestamp': datetime.datetime.now().isoformat(),
            'output': serializable_data
        }
        with open(output_path, 'w') as file:
            json.dump(data, file)
        
        # ?? delete image wasnt used
        temp_img_dir = os.path.join(temp_dir, 'temp')
        if os.path.exists(temp_img_dir):
            used_images = set()
            for output in serializable_data:
                result = output.get("result", {})
                plot_path = result.get("Plot")
                if plot_path and isinstance(plot_path, str):
                    used_images.add(os.path.abspath(plot_path))
            for output in self.data:
                result = output.get("result", {})
                plot_paths = result.get("Plot")
                if isinstance(plot_paths, list):
                    for p in plot_paths:
                        used_images.add(os.path.abspath(p))
                elif isinstance(plot_paths, str):
                    used_images.add(os.path.abspath(plot_paths))
            # Hapus file di temp yang tidak digunakan
            for fname in os.listdir(temp_img_dir):
                fpath = os.path.abspath(os.path.join(temp_img_dir, fname))
                if fpath not in used_images:
                    try:
                        os.remove(fpath)
                    except Exception:
                        pass
        if self.isActiveWindow():
            self.show_toast()
        

    def load_temp_data(self):
        """
        Load data1 and data2 from parquet, and output from JSON, if they exist.
        """
        app_path = os.path.join(os.getenv("APPDATA"), "saePisan")
        temp_dir = os.path.join(app_path, 'file-data')
        data1_path = os.path.join(temp_dir, 'sae_pisan_data1.parquet')
        data2_path = os.path.join(temp_dir, 'sae_pisan_data2.parquet')
        output_path = os.path.join(temp_dir, 'sae_pisan_output.json')
        if os.path.exists(data1_path) and os.path.exists(data2_path) and os.path.exists(output_path):
            reply = QMessageBox.question(self, 'Load Temporary Data',
                                         'Temporary data was found. Do you want to load it?',
                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                         QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.Yes:
                self.data1 = pl.read_parquet(data1_path)
                self.data2 = pl.read_parquet(data2_path)
                self.model1.set_data(self.data1)
                self.model2.set_data(self.data2)
                self.update_table(1, self.model1)
                self.update_table(2, self.model2)
                with open(output_path, 'r') as file:
                    data = json.load(file)
                    self.set_output_data(data.get('output', []), timestamp=data.get('timestamp'))
        else:
            QMessageBox.warning(self, 'No Recent Data', 'No recent data file was found.')

    def set_output_data(parent, output_data, timestamp=None):
        """
        Menampilkan kembali output card dari data hasil get_output_data.
        """
        for output in output_data:
            r_script = output.get("r_script", "")
            results = output.get("result", "")
            for key, value in results.items():
                if isinstance(value, dict):
                    df = pl.DataFrame(value)
                    results[key] = df
                else:
                    results[key] = value
            if "Plot" in results:
                display_script_and_output(parent, r_script, results, plot_paths=results["Plot"], timestamps=timestamp)
            else:
                display_script_and_output(parent, r_script, results, plot_paths=None, timestamps=timestamp)
        
    def show_header_icon_info(self):
        """
        Show a dialog explaining the meaning of header icons in the data table.
        """
        msg = (
            "<b>Header Icon Legend:</b><br><br>"
            "<div style='display:flex; flex-direction:column; gap:8px;'>"
            "<div><img src='assets/nominal.svg' width='24' height='24' style='vertical-align:middle;'> <b>Nominal/String</b></div>"
            "<div><img src='assets/null.svg' width='24' height='24' style='vertical-align:middle;'> <b>Null/Empty</b></div>"
            "<div><img src='assets/numeric.svg' width='24' height='24' style='vertical-align:middle;'> <b>Numeric</b></div>"
            "</div><br>"
            "<div style='max-width:350px;'>"
            "These icons indicate the data type of each column in the table. "
            "Nominal/String columns are represented by the nominal icon, "
            "Null/Empty columns are represented by the null icon, and "
            "Numeric columns are represented by the numeric icon."
            "</div>"
        )
        QMessageBox.information(self, "Header Icon Info", msg)
    
    def show_r_packages_info(self):
        """
        Show a dialog listing the R packages used and their versions in a single table with 6 columns (3 pairs of Package/Version side by side).
        """
        try:
            import rpy2.robjects as ro
            packages = [
                "sae", "arrow", "sae.projection", "emdi", "xgboost", "LiblineaR",
                "kernlab", "GGally", "ggplot2", "ggcorrplot", "car", "nortest",
                "tidyr", "carData", "dplyr", "tseries", "FSelector", "rjags", "saeHB"
            ]
            versions = []
            for pkg in packages:
                try:
                    ver = ro.r(f"as.character(packageVersion('{pkg}'))")[0]
                except Exception:
                    ver = "Not Installed"
                versions.append((pkg, ver))

            # Arrange into 3 columns (each column is Package/Version pair)
            n_cols = 3
            n_rows = (len(versions) + n_cols - 1) // n_cols
            table_cells = []
            for i in range(n_rows):
                row = []
                for j in range(n_cols):
                    idx = i + j * n_rows
                    if idx < len(versions):
                        row.extend(versions[idx])
                    else:
                        row.extend(("", ""))
                table_cells.append(row)

            msg = "<b>R Packages Used:</b><br><br>"
            msg += "<table style='border-collapse:collapse;font-size:13px;'>"
            # Header
            msg += "<tr style='background-color:#f2f2f2;'>"
            for i in range(n_cols):
                msg += "<th style='border:1px solid #bbb; padding:6px 16px;'>Package</th>"
                msg += "<th style='border:1px solid #bbb; padding:6px 16px;'>Version</th>"
            msg += "</tr>"
            # Rows
            for row in table_cells:
                msg += "<tr>"
                for cell in row:
                    msg += f"<td style='border:1px solid #bbb; padding:6px 16px;'>{cell}</td>"
                msg += "</tr>"
            msg += "</table>"
        except Exception as e:
            msg = f"Could not retrieve R package versions.<br>Error: {e}"
        QMessageBox.information(self, "R Packages Used", msg)
            
    def show_toast(self):
        toast = CustomToast(
            parent=self,
            title="Saved",
            text="Data, Data Output, and Output was saved",
            duration=3000,
            position="top-right"
        )
        toast.set_border_radius(8)
        toast.show()
    
    def closeEvent(self, event):
        """Handle the close event to show a confirmation dialog."""
        reply = QMessageBox.question(self, 'Confirm Exit',
                                     'Are you sure you want to exit?',
                                     QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                     QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            self.autosave_data()
            import rpy2.robjects as ro
            if 'saeHB' in ro.r('loadedNamespaces()'):
                ro.r('detach("package:saeHB", unload=TRUE)')
            if 'rjags' in ro.r('loadedNamespaces()'):
                ro.r("unloadNamespace('rjags')")
            
            #to kill the process
            import os
            import psutil

            current_system_pid = os.getpid()

            ThisSystem = psutil.Process(current_system_pid)
            ThisSystem.terminate()
            event.accept()
        else:
            event.ignore()

Main application window for SAE Pisan: Small Area Estimation Programming for Statistical Analysis.

Attributes

data1 : pl.DataFrame
DataFrame for the first sheet (Data Editor).
data2 : pl.DataFrame
DataFrame for the second sheet (Data Output).
model1 : TableModel
Table model for the first sheet.
model2 : TableModel
Table model for the second sheet.
path : str
Path to the application directory.
font_size : int
Default font size for the application.
show_modeling_sae_dialog : ModelingSaeDialog
Dialog for SAE modeling.
show_modeling_saeHB_dialog : ModelingSaeHBDialog
Dialog for SAE HB modeling.
show_modeling_sae_unit_dialog : ModelingSaeUnitDialog
Dialog for SAE unit modeling.
show_modeling_saeHB_normal_dialog : ModelingSaeHBNormalDialog
Dialog for SAE HB normal modeling.
show_modellig_sae_pseudo_dialog : ModelingSaePseudoDialog
Dialog for SAE pseudo modeling.
show_compute_variable_dialog : ComputeVariableDialog
Dialog for computing new variables.
show_projection_variabel_dialog : ProjectionDialog
Dialog for variable projection.
menu_bar : QMenuBar
Menu bar for the application.
toolBar : QToolBar
Tool bar for the application.
tab_widget : QTabWidget
Tab widget containing the different sheets.
spreadsheet : QTableView
Table view for the first sheet.
table_view2 : QTableView
Table view for the second sheet.
scroll_area : QScrollArea
Scroll area for the output tab.
output_layout : QVBoxLayout
Layout for displaying output in the output tab.

Methods

init_ui(): Initializes the user interface. change_font_size(): Opens a dialog to change the font size. set_font_size(size): Sets the font size for the application. load_stylesheet_with_font_size(size): Loads the stylesheet with the specified font size. open_summary_data_dialog_lazy(): Lazily opens the summary data dialog. open_normality_test_dialog_lazy(): Lazily opens the normality test dialog. open_multicollinearity_dialog_lazy(): Lazily opens the multicollinearity dialog. open_variable_selection_dialog_lazy(): Lazily opens the variable selection dialog. open_scatter_plot_dialog_lazy(): Lazily opens the scatter plot dialog. open_correlation_matrix_dialog_lazy(): Lazily opens the correlation matrix dialog. open_box_plot_dialog_lazy(): Lazily opens the box plot dialog. open_line_plot_dialog_lazy(): Lazily opens the line plot dialog. open_histogram_dialog_lazy(): Lazily opens the histogram dialog. open_summary_data_dialog(): Opens the summary data dialog. open_normality_test_dialog(): Opens the normality test dialog. open_scatter_plot_dialog(): Opens the scatter plot dialog. open_line_plot_dialog(): Opens the line plot dialog. open_box_plot_dialog(): Opens the box plot dialog. open_correlation_matrix_dialog(): Opens the correlation matrix dialog. open_multicollinearity_dialog(): Opens the multicollinearity dialog. open_histogram_dialog(): Opens the histogram dialog. open_variable_selection_dialog(): Opens the variable selection dialog. show_modeling_sae_dialog_lazy(): Lazily shows the SAE modeling dialog. show_modeling_saeHB_dialog_lazy(): Lazily shows the SAE HB modeling dialog. show_modeling_sae_unit_dialog_lazy(): Lazily shows the SAE unit modeling dialog. show_modeling_saeHB_normal_dialog_lazy(): Lazily shows the SAE HB normal modeling dialog. show_modellig_sae_pseudo_dialog_lazy(): Lazily shows the SAE pseudo modeling dialog. show_compute_variable_dialog_lazy(): Lazily shows the compute variable dialog. show_projection_variabel_dialog_lazy(): Lazily shows the projection variable dialog. open_about_dialog(): Opens the about dialog. add_row(sheet_number): Adds a new row to the specified sheet. add_column(sheet_number): Adds a new column to the specified sheet. update_table(sheet_number, model): Updates the table for the specified sheet with a new model. keyPressEvent(event): Handles keyboard shortcuts for copy, paste, undo, and redo. copy_selection(): Copies the selected cells to the clipboard. paste_selection(): Pastes the clipboard content to the selected cells. undo_action(): Undoes the last action. redo_action(): Redoes the last undone action. group_by_row(selection): Groups selected indexes by row. show_output(title, content): Displays output in the Output tab. show_header_context_menu(pos): Shows the context menu for the header. rename_column(column_index): Renames the column at the given index. edit_data_type(column_index): Edits the data type of the column at the given index. set_path(path): Sets the path for the application. add_output(script_text, result_text=None, plot_paths=None, error_text=None): Adds output to the layout in the form of a card. remove_output(card_frame): Removes output from the layout. copy_output_image(card_frame): Copies the output image to the clipboard. show_context_menu(pos, card_frame): Shows the context menu for each output.

Initializes the MainWindow class. This constructor sets up the main window for the SAE Pisan application, including setting the window title, initializing data frames, models, and UI components.

Attributes

data1 : pl.DataFrame
A DataFrame with 100 columns and 100 empty rows.
data2 : pl.DataFrame
A DataFrame with columns "Estimated Value", "Standar Error", and "CV", each with 100 empty rows.
model1 : TableModel
The table model for the first data frame.
model2 : TableModel
The table model for the second data frame.
path : str
The path to the parent directory of the current file.
font_size : int
The font size used in the UI.

Ancestors

  • PyQt6.QtWidgets.QMainWindow
  • PyQt6.QtWidgets.QWidget
  • PyQt6.QtCore.QObject
  • PyQt6.sip.wrapper
  • PyQt6.QtGui.QPaintDevice
  • PyQt6.sip.simplewrapper

Methods

def add_column(self, sheet_number)
Expand source code
def add_column(self, sheet_number):
    """Sinkronisasi data ketika kolom baru ditambahkan di SpreadsheetWidget."""
    if sheet_number == 1:
        # Tambahkan kolom baru di DataFrame data1
        new_column_name = f"Column {self.data1.shape[1] + 1}"
        new_column = pl.DataFrame({new_column_name: [""] * self.data1.shape[0]})
        self.data1 = pl.concat([self.data1, new_column], how="horizontal")
    elif sheet_number == 2:
        pass  # Tidak digunakan untuk Sheet 2

Sinkronisasi data ketika kolom baru ditambahkan di SpreadsheetWidget.

def add_output(self, script_text, result_text=None, plot_paths=None, error_text=None)
Expand source code
def add_output(self, script_text, result_text=None, plot_paths=None, error_text=None):
    """Add output to the layout in the form of a card"""

    # Create a frame as a card
    card_frame = QFrame()
    card_frame.setStyleSheet("""
        QFrame {
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 10px;
        }
    """)


    card_layout = QVBoxLayout(card_frame)
    card_layout.setSpacing(8)

    label_script = QLabel("<b>R Script:</b>")
    label_script.setStyleSheet("color: #333; margin-bottom: 5px;")
    script_box = QTextEdit()
    script_box.setPlainText(script_text)
    script_box.setReadOnly(True)
    script_box.setStyleSheet("""
        QTextEdit {
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 5px;
            font-family: Consolas, Courier New, monospace;
        }
    """)
    script_box.setFixedHeight(script_box.fontMetrics().lineSpacing() * (script_text.count('\n') + 3))

    card_layout.addWidget(label_script)
    card_layout.addWidget(script_box)

    if result_text:
        label_output = QLabel("<b>Output:</b>")
        label_output.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
        result_box = QTextEdit()
        result_box.setPlainText(result_text)
        result_box.setReadOnly(True)
        result_box.setStyleSheet("""
            QTextEdit {
                background-color: #fff;
                border: 1px solid #ccc;
                border-radius: 4px;
                padding: 5px;
                font-family: Consolas, Courier New, monospace;
            }
        """)
        max_height = 400
        calculated_height = result_box.fontMetrics().lineSpacing() * (result_text.count('\n') + 3)
        result_box.setFixedHeight(min(calculated_height, max_height))
        result_box.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn if calculated_height > max_height else Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

        card_layout.addWidget(label_output)
        card_layout.addWidget(result_box)

    if plot_paths:
        label_plot = QLabel("<b>Plot:</b>")
        label_plot.setStyleSheet("color: #333; margin-top: 10px; margin-bottom: 5px;")
        card_layout.addWidget(label_plot)

        for plot_path in plot_paths:
            if os.path.exists(plot_path):
                pixmap = QPixmap(plot_path)
                label = QLabel()
                label.setPixmap(pixmap)
                label.setFixedSize(500, 350)
                label.setScaledContents(True)
                label.setStyleSheet("border: 1px solid #ccc; border-radius: 4px;")
                card_layout.addWidget(label)

    if error_text:
        label_error = QLabel("<b>Error:</b>")
        label_error.setStyleSheet("color: #a94442; margin-top: 10px; margin-bottom: 5px;")
        error_box = QTextEdit()
        error_box.setPlainText(error_text)
        error_box.setReadOnly(True)
        error_box.setStyleSheet("""
            QTextEdit {
                background-color: #f8d7da;
                border: 1px solid #f5c6cb;
                border-radius: 4px;
                padding: 5px;
                font-family: Consolas, Courier New, monospace;
                color: #721c24;
            }
        """)
        error_box.setFixedHeight(error_box.fontMetrics().lineSpacing() * (error_text.count('\n') + 3))

        card_layout.addWidget(label_error)
        card_layout.addWidget(error_box)

    card_frame.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    card_frame.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos, card_frame))

    if self.output_layout.count() > 0:
        last_item = self.output_layout.itemAt(self.output_layout.count() - 1)
        if isinstance(last_item.spacerItem(), QSpacerItem):
            self.output_layout.removeItem(last_item)

    self.output_layout.addWidget(card_frame)
    if self.output_layout.count() == 1:
        self.output_layout.addStretch()

    self.tab_widget.setCurrentWidget(self.tab3)

    if plot_paths:
        for plot_path in plot_paths:
            if os.path.exists(plot_path):
                os.remove(plot_path)

Add output to the layout in the form of a card

def add_row(self, sheet_number)
Expand source code
def add_row(self, sheet_number):
    """Sinkronisasi data ketika baris baru ditambahkan di SpreadsheetWidget."""
    if sheet_number == 1:
        # Tambahkan baris baru di DataFrame data1
        new_row = pl.DataFrame({col: [""] for col in self.data1.columns})
        self.data1 = pl.concat([self.data1, new_row])
    elif sheet_number == 2:
        pass  # Tidak digunakan untuk Sheet 2

Sinkronisasi data ketika baris baru ditambahkan di SpreadsheetWidget.

def autosave_data(self)
Expand source code
def autosave_data(self):
    """
    Save the current state of data1, data2 (as parquet), and output (as JSON) to temporary files.
    """
    app_data_dir = os.path.join(os.getenv("APPDATA"), "saePisan")
    os.makedirs(app_data_dir, exist_ok=True)
    temp_dir = os.path.join(app_data_dir, 'file-data')
    os.makedirs(temp_dir, exist_ok=True)
    # Save data1 and data2 as parquet
    data1_path = os.path.join(temp_dir, 'sae_pisan_data1.parquet')
    data2_path = os.path.join(temp_dir, 'sae_pisan_data2.parquet')
    self.model1.get_data().write_parquet(data1_path)
    self.model2.get_data().write_parquet(data2_path)
    # Save output as JSON
    output_path = os.path.join(temp_dir, 'sae_pisan_output.json')
    import numpy as np

    def make_json_serializable(obj):
        if isinstance(obj, dict):
            return {k: make_json_serializable(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [make_json_serializable(v) for v in obj]
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        elif hasattr(obj, 'tolist'):
            return obj.tolist()
        elif isinstance(obj, (int, float, str, type(None), bool)):
            return obj
        else:
            return str(obj)

    serializable_data = make_json_serializable(self.data)
    data = {
        'timestamp': datetime.datetime.now().isoformat(),
        'output': serializable_data
    }
    with open(output_path, 'w') as file:
        json.dump(data, file)
    
    # ?? delete image wasnt used
    temp_img_dir = os.path.join(temp_dir, 'temp')
    if os.path.exists(temp_img_dir):
        used_images = set()
        for output in serializable_data:
            result = output.get("result", {})
            plot_path = result.get("Plot")
            if plot_path and isinstance(plot_path, str):
                used_images.add(os.path.abspath(plot_path))
        for output in self.data:
            result = output.get("result", {})
            plot_paths = result.get("Plot")
            if isinstance(plot_paths, list):
                for p in plot_paths:
                    used_images.add(os.path.abspath(p))
            elif isinstance(plot_paths, str):
                used_images.add(os.path.abspath(plot_paths))
        # Hapus file di temp yang tidak digunakan
        for fname in os.listdir(temp_img_dir):
            fpath = os.path.abspath(os.path.join(temp_img_dir, fname))
            if fpath not in used_images:
                try:
                    os.remove(fpath)
                except Exception:
                    pass
    if self.isActiveWindow():
        self.show_toast()

Save the current state of data1, data2 (as parquet), and output (as JSON) to temporary files.

def change_font_size(self)
Expand source code
def change_font_size(self):
    """
    Opens a dialog to change the font size of the application.
    The dialog presents three font size options: "Small", "Medium", and "Big".
    The user can select a font size from a combo box, and the selected size
    will be applied to the application if the user confirms the selection.
    The dialog also displays a sample text ("AaBbCc") that updates in real-time
    to reflect the selected font size.
    Attributes:
        sizes (dict): A dictionary mapping font size names to their corresponding pixel values.
        items (list): A list of font size names.
        dialog (QDialog): The dialog window for selecting the font size.
        layout (QVBoxLayout): The main layout of the dialog.
        combo_box (QComboBox): The combo box for selecting the font size.
        display (QLabel): The label displaying the sample text with the selected font size.
        button_box (QHBoxLayout): The layout containing the OK and Cancel buttons.
        ok_button (QPushButton): The button to confirm the font size selection.
        cancel_button (QPushButton): The button to cancel the font size selection.
    Methods:
        update_display_font_size: Updates the sample text's font size based on the selected size in the combo box.
    """
    
    sizes = {"Small": 10, "Medium": 12, "Big": 16}
    items = list(sizes.keys())

    dialog = QDialog(self)
    dialog.setWindowTitle("Select Font Size")
    dialog.setMinimumWidth(200)

    layout = QVBoxLayout(dialog)

    combo_box = QComboBox()
    combo_box.addItems(items)
    default_size = next(key for key, value in sizes.items() if value == self.font_size)
    combo_box.setCurrentText(default_size)
    layout.addWidget(combo_box)
    
    display = QLabel("AaBbCc")
    display.setAlignment(Qt.AlignmentFlag.AlignCenter)
    display.setStyleSheet(f"font-size: {self.font_size}px;")
    layout.addWidget(display)

    def update_display_font_size():
        size = combo_box.currentText()
        display.setStyleSheet(f"font-size: {sizes[size]}px;")

    combo_box.currentTextChanged.connect(update_display_font_size)

    button_box = QHBoxLayout()
    ok_button = QPushButton("OK")
    cancel_button = QPushButton("Cancel")
    button_box.addWidget(ok_button)
    button_box.addWidget(cancel_button)
    layout.addLayout(button_box)

    ok_button.clicked.connect(dialog.accept)
    cancel_button.clicked.connect(dialog.reject)

    dialog.setLayout(layout)

    if dialog.exec() == QDialog.DialogCode.Accepted:
        selected_size = combo_box.currentText()
        self.set_font_size(sizes[selected_size])
        self.font_size = sizes[selected_size]

Opens a dialog to change the font size of the application. The dialog presents three font size options: "Small", "Medium", and "Big". The user can select a font size from a combo box, and the selected size will be applied to the application if the user confirms the selection. The dialog also displays a sample text ("AaBbCc") that updates in real-time to reflect the selected font size.

Attributes

sizes : dict
A dictionary mapping font size names to their corresponding pixel values.
items : list
A list of font size names.
dialog : QDialog
The dialog window for selecting the font size.
layout : QVBoxLayout
The main layout of the dialog.
combo_box : QComboBox
The combo box for selecting the font size.
display : QLabel
The label displaying the sample text with the selected font size.
button_box : QHBoxLayout
The layout containing the OK and Cancel buttons.
ok_button : QPushButton
The button to confirm the font size selection.
cancel_button : QPushButton
The button to cancel the font size selection.

Methods

update_display_font_size: Updates the sample text's font size based on the selected size in the combo box.

def closeEvent(self, event)
Expand source code
def closeEvent(self, event):
    """Handle the close event to show a confirmation dialog."""
    reply = QMessageBox.question(self, 'Confirm Exit',
                                 'Are you sure you want to exit?',
                                 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                 QMessageBox.StandardButton.No)
    if reply == QMessageBox.StandardButton.Yes:
        self.autosave_data()
        import rpy2.robjects as ro
        if 'saeHB' in ro.r('loadedNamespaces()'):
            ro.r('detach("package:saeHB", unload=TRUE)')
        if 'rjags' in ro.r('loadedNamespaces()'):
            ro.r("unloadNamespace('rjags')")
        
        #to kill the process
        import os
        import psutil

        current_system_pid = os.getpid()

        ThisSystem = psutil.Process(current_system_pid)
        ThisSystem.terminate()
        event.accept()
    else:
        event.ignore()

Handle the close event to show a confirmation dialog.

def copy_output_image(self, card_frame)
Expand source code
def copy_output_image(self, card_frame):
    """Menyalin gambar output ke clipboard"""
    for child in card_frame.findChildren(QLabel):
        pixmap = child.pixmap()
        
        if pixmap and not pixmap.isNull():
            temp_folder = os.path.join(self.path, 'temp')
            temp_path = os.path.join(temp_folder, 'temp_image.png')

            os.makedirs(temp_folder, exist_ok=True)

            if pixmap.save(temp_path):
                print(f"Gambar disimpan di: {temp_path}")
                
                clipboard = QApplication.clipboard()
                clipboard.setPixmap(QPixmap(temp_path))

                if os.path.exists(temp_path):  
                    os.remove(temp_path)
            break

Menyalin gambar output ke clipboard

def copy_selection(self)
Expand source code
def copy_selection(self):
    """Copy selected cells to clipboard."""
    selection = self.spreadsheet.selectionModel().selectedIndexes()
    if selection:
        data = '\n'.join(['\t'.join([self.model1.data(index, Qt.ItemDataRole.DisplayRole) for index in row]) for row in self.group_by_row(selection)])
        clipboard = QApplication.clipboard()
        clipboard.setText(data)

Copy selected cells to clipboard.

def edit_data_type(self, column_index)
Expand source code
def edit_data_type(self, column_index):
    """Edit the data type of the column at the given index."""
    current_type = self.model1.get_column_type(column_index)
    if current_type==pl.Utf8:
        current_type = "String"
    elif current_type==pl.Int64:
        current_type = "Integer"
    elif current_type==pl.Float64:
        current_type = "Float"
    type_list = ["String", "Integer", "Float"]
    current_index = type_list.index(current_type)
    new_type, ok = QInputDialog.getItem(self, "Edit Data Type", "Select new data type:", type_list, current=current_index)
    if ok and new_type:
        self.model1.set_column_type(column_index, new_type)
        self.update_table(1, self.model1)

Edit the data type of the column at the given index.

def group_by_row(self, selection)
Expand source code
def group_by_row(self, selection):
    """Group selected indexes by row."""
    rows = {}
    for index in selection:
        if index.row() not in rows:
            rows[index.row()] = []
        rows[index.row()].append(index)
    return [rows[row] for row in sorted(rows)]

Group selected indexes by row.

def init_ui(self)
Expand source code
def init_ui(self):
    """
    Initialize the user interface of the main window.
    This method sets up the main layout, including a splitter, tab widgets, 
    and various tabs for data editing, data output, and other functionalities. 
    It also configures the menu bar with different menus and actions, 
    and sets up the toolbar with various actions and icons.
    Tabs:
        - Data Editor: Allows editing of data in a spreadsheet format.
        - Data Output: Displays output data in a table view.
        - Output: Displays output in a scrollable area.
    Menus:
        - File: Contains actions to load and save files.
        - Exploration: Contains actions for data exploration such as summary data, normality test, correlation, etc.
        - Graph: Contains actions to generate different types of plots.
        - Model: Contains actions related to different modeling techniques.
        - Compute: Contains actions to compute new variables.
        - About: Contains information about the application.
        - Settings: Contains actions to change application settings.
    Toolbar:
        - Load File: Action to load a file.
        - Save Data: Action to save data.
        - Undo: Action to undo the last operation.
        - Redo: Action to redo the last undone operation.
        - Compute New Variable: Action to compute a new variable.
        - Setting: Action to open settings.
    Shortcuts:
        - Go to Start Row: Ctrl + Up
        - Go to End Row: Ctrl + Down
        - Go to Start Column: Ctrl + Left
        - Go to End Column: Ctrl + Right
    Layout:
        - The main layout is a vertical box layout containing the tab widget.
        - The central widget is set with this layout.
        - The main window is resized to a default size of 800x600.
    """
    
    # Membuat splitter utama untuk membagi halaman menjadi dua bagian (kiri dan kanan)
    self.splitter_main = QSplitter(Qt.Orientation.Horizontal, self)

    # Bagian kiri: QTabWidget untuk dua sheet
    self.tab_widget = QTabWidget(self.splitter_main)  # Ditambahkan ke splitter utama
    self.tab_widget.setTabPosition(QTabWidget.TabPosition.South)
    
    self.show_modeling_sae_dialog = None
    self.show_modeling_saeHB_dialog = None
    self.show_modeling_sae_unit_dialog = None
    self.show_modeling_saeHB_normal_dialog = None
    self.show_modellig_sae_pseudo_dialog = None
    self.show_compute_variable_dialog = None
    self.show_projection_variabel_dialog = None
    

    # Tab pertama (Data Editor)
    self.tab1 = QWidget()
    self.spreadsheet = QTableView(self.tab1)
    self.spreadsheet.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.spreadsheet.customContextMenuRequested.connect(lambda pos: show_context_menu(self, pos))
    self.spreadsheet.setModel(self.model1)
    self.spreadsheet.horizontalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.spreadsheet.horizontalHeader().customContextMenuRequested.connect(self.show_header_context_menu)
    self.spreadsheet.horizontalHeader().sectionDoubleClicked.connect(self.rename_column)
    self.spreadsheet.verticalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.spreadsheet.verticalHeader().customContextMenuRequested.connect(lambda pos: show_context_menu(self, pos))

    tab1_layout = QVBoxLayout(self.tab1)
    tab1_layout.addWidget(self.spreadsheet)
    self.spreadsheet.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

    # Tab kedua (Data Output)
    self.tab2 = QWidget()
    self.table_view2 = QTableView(self.tab2)
    self.table_view2.setModel(self.model2)
    self.table_view2.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)

    tab2_layout = QVBoxLayout(self.tab2)
    tab2_layout.addWidget(self.table_view2)

    # Tab ketiga (Output)
    self.tab3 = QWidget()
    self.scroll_area = QScrollArea(self.tab3)
    
    # Tab output
    self.output_tab = QWidget()
    self.scroll_area = QScrollArea(self.output_tab)
    self.scroll_area.setWidgetResizable(True)
    self.output_container = QWidget()
    self.output_layout = QVBoxLayout(self.output_container)
    self.scroll_area.verticalScrollBar().rangeChanged.connect(lambda: self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().maximum()))
    self.output_container.setLayout(self.output_layout)
    self.scroll_area.setWidget(self.output_container)
    
    # Atur layout output
    self.output_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
    self.output_layout.setSpacing(0)

    tab3_layout = QVBoxLayout(self.tab3)
    tab3_layout.addWidget(self.scroll_area)

    # Menambahkan tab ke QTabWidget
    self.tab_widget.addTab(self.tab1, "Data Editor")
    self.tab_widget.addTab(self.tab2, "Data Output")
    self.tab_widget.addTab(self.tab3, "Output")  # Tab baru untuk output

    # Membuat layout utama
    layout = QVBoxLayout()
    layout.addWidget(self.tab_widget)
    

    # Widget utama dan layout
    central_widget = QWidget(self)
    central_widget.setLayout(layout)
    self.setCentralWidget(central_widget)

    # Membuat menu bar
    self.menu_bar = self.menuBar()

    # Membuat menu File -> Load dan Save
    self.file_menu = self.menu_bar.addMenu("File")
    
    self.recent_data = QAction("Open Recent data", self)
    self.recent_data.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_D))
    self.recent_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'recentdata.svg')))
    self.recent_data.setStatusTip("Ctrl+D")
    
    self.load_action = QAction("Load File", self)
    self.load_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_O))
    self.load_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'open.svg')))
    self.load_action.setStatusTip("Ctrl+O")
    
    self.load_secondary_data = QAction("Load File for Secondary Data", self)
    self.load_secondary_data.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_2))
    self.load_secondary_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'secondary.svg')))
    self.load_secondary_data.setStatusTip("Ctrl+2")
    
    self.save_action = QAction("Save Data", self)
    self.save_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_S))
    self.save_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savedata.svg')))
    self.save_action.setStatusTip("Ctrl+S")
    
    self.save_data_output_action = QAction("Save Data Output", self)
    self.save_data_output_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_S))
    self.save_data_output_action.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savedataoutput.svg')))
    self.save_action.setStatusTip("Ctrl+Shift+S")
    
    self.save_output_pdf = QAction("Save Output to PDF", self)
    self.save_output_pdf.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_P))
    self.save_output_pdf.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'savepdf.svg')))
    self.save_output_pdf.setStatusTip("Ctrl+P")

    self.file_menu.addAction(self.recent_data)
    self.file_menu.addAction(self.load_action)
    self.file_menu.addAction(self.load_secondary_data)
    self.file_menu.addAction(self.save_action)
    self.file_menu.addAction(self.save_data_output_action)
    self.file_menu.addAction(self.save_output_pdf)

    # Menu "Exploration"
    self.menu_exploration = self.menu_bar.addMenu("Pre-Modeling")

    self.action_summary_data = QAction("Data Summary", self)
    self.action_summary_data.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'summary.svg')))
    self.action_summary_data.triggered.connect(self.open_summary_data_dialog_lazy)

    self.action_normality_test = QAction("Normality Test", self)
    self.action_normality_test.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'normality.svg')))
    self.action_normality_test.triggered.connect(self.open_normality_test_dialog_lazy)

    self.action_correlation = QAction("Correlation", self)
    self.action_correlation.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'correlation.svg')))
    self.action_correlation.triggered.connect(self.open_correlation_matrix_dialog_lazy)

    self.action_multicollinearity = QAction("Multicollinearity", self)
    self.action_multicollinearity.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'multicolinierity.svg')))
    self.action_multicollinearity.triggered.connect(self.open_multicollinearity_dialog_lazy)

    self.action_variable_selection = QAction("Variable Selection", self)
    self.action_variable_selection.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'varselection.svg')))
    self.action_variable_selection.triggered.connect(self.open_variable_selection_dialog_lazy)

    self.menu_exploration.addAction(self.action_summary_data)
    self.menu_exploration.addAction(self.action_normality_test)
    self.menu_exploration.addAction(self.action_correlation)
    self.menu_exploration.addAction(self.action_multicollinearity)
    self.menu_exploration.addAction(self.action_variable_selection)

    # Menu "Graph"
    self.menu_graph = self.menu_bar.addMenu("Graph")

    self.action_scatter_plot = QAction("Scatter Plot", self)
    self.action_scatter_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'scatterplot.svg')))
    self.action_scatter_plot.triggered.connect(self.open_scatter_plot_dialog_lazy)

    self.action_box_plot = QAction("Box Plot", self)
    self.action_box_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'boxplot.svg')))
    self.action_box_plot.triggered.connect(self.open_box_plot_dialog_lazy)

    self.action_line_plot = QAction("Line Plot", self)
    self.action_line_plot.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'lineplot.svg')))
    self.action_line_plot.triggered.connect(self.open_line_plot_dialog_lazy)

    self.action_histogram = QAction("Histogram", self)
    self.action_histogram.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'histogram.svg')))
    self.action_histogram.triggered.connect(self.open_histogram_dialog_lazy)

    # Menambahkan plot-plot ke menu Graph
    self.menu_graph.addAction(self.action_scatter_plot)
    self.menu_graph.addAction(self.action_box_plot)
    self.menu_graph.addAction(self.action_line_plot)
    self.menu_graph.addAction(self.action_histogram)
    # Menu "Model"
    menu_model = self.menu_bar.addMenu("Model")

    # Submenu "Area Level"
    menu_area_level = QMenu("Area Level", self)
    menu_area_level.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'arealevel.svg')))
    action_eblup_area = QAction("EBLUP", self)
    action_eblup_area.triggered.connect(self.show_modeling_sae_dialog_lazy)
    action_hb_beta = QAction("HB Beta", self)
    action_hb_beta.triggered.connect(self.show_modeling_saeHB_dialog_lazy)
    menu_area_level.addAction(action_eblup_area)
    menu_area_level.addAction(action_hb_beta)

    # Submenu "Unit Level"
    menu_unit_level = QMenu("Unit Level", self)
    menu_unit_level.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'unitlevel.svg')))
    action_eblup_unit = QAction("EBLUP", self)
    action_eblup_unit.triggered.connect(self.show_modeling_sae_unit_dialog_lazy)
    action_hb_normal = QAction("HB Normal", self)
    action_hb_normal.triggered.connect(self.show_modeling_saeHB_normal_dialog_lazy)
    menu_unit_level.addAction(action_eblup_unit)
    menu_unit_level.addAction(action_hb_normal)

    # Submenu "Pseudo"
    menu_pseudo = QMenu("Pseudo", self)
    menu_pseudo.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'pseudo.svg')))
    action_eblup_pseudo = QAction("EBLUP", self)
    action_eblup_pseudo.triggered.connect(self.show_modellig_sae_pseudo_dialog_lazy)
    menu_pseudo.addAction(action_eblup_pseudo)

    # Submenu "Projection"
    menu_projection = QMenu("Projection", self)
    menu_projection.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'projection.svg')))
    action_projection = QAction("Projection", self)
    action_projection.triggered.connect(self.show_projection_variabel_dialog_lazy)
    menu_projection.addAction(action_projection)


    # Menambahkan submenu ke menu "Model"
    menu_model.addMenu(menu_area_level)
    menu_model.addMenu(menu_unit_level)
    menu_model.addMenu(menu_pseudo)
    menu_model.addMenu(menu_projection)



     # Menu 'Compute'
    menu_compute = self.menu_bar.addMenu("Compute")
    compute_new_var = QAction("Compute New Variable", self)
    compute_new_var.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'compute.svg')))
    compute_new_var.triggered.connect(self.show_compute_variable_dialog_lazy)
    menu_compute.addAction(compute_new_var)
    
    # Menu "About"
    menu_about = self.menu_bar.addMenu("About")
    action_about_info = QAction("About This App", self)
    action_about_info.triggered.connect(self.open_about_dialog)
    action_about_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'about.svg')))
    menu_about.addAction(action_about_info)
    
    action_header_icon_info = QAction("Header Icon Info", self)
    action_header_icon_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'information.svg')))
    action_header_icon_info.triggered.connect(self.show_header_icon_info)
    menu_about.addAction(action_header_icon_info)
    
    # Add R Packages Used menu
    action_r_packages_info = QAction("R Packages Used", self)
    action_r_packages_info.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'Rinfo.svg')))
    action_r_packages_info.triggered.connect(self.show_r_packages_info)
    menu_about.addAction(action_r_packages_info)
    

    # Tool Bar
    self.toolBar = QToolBar(self)
    self.toolBar.setIconSize(QSize(45, 35))
    self.toolBar.setObjectName("toolBar")
    self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolBar)  # Perbaikan dilakukan di sini
    # Actions for Toolbar
    self.actionLoad_file = QAction(self)  # Menggunakan self untuk referensi instance
    icon_load = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'open.svg'))
    self.actionLoad_file.setIcon(icon_load)
    self.actionLoad_file.setText("Load File")
    self.toolBar.addAction(self.actionLoad_file)

    self.actionSave_Data = QAction(self)  # Menggunakan self untuk referensi instance
    icon_save = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'save.svg'))
    self.actionSave_Data.setIcon(icon_save)
    self.actionSave_Data.setText("Save Data")
    self.toolBar.addAction(self.actionSave_Data)

    self.actionUndo = QAction(self)
    icon_undo = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'undo.svg'))
    self.actionUndo.setIcon(icon_undo)
    self.actionUndo.setText("Undo")
    self.actionUndo.triggered.connect(self.undo_action)
    self.toolBar.addAction(self.actionUndo)

    self.actionRedo = QAction(self)
    icon_redo = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'redo.svg'))
    self.actionRedo.setIcon(icon_redo)
    self.actionRedo.setText("Redo")
    self.actionRedo.triggered.connect(self.redo_action)
    self.toolBar.addAction(self.actionRedo)
    
    self.actionCompute = QAction(self)
    icon_compute = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'compute.svg'))
    self.actionCompute.setIcon(icon_compute)
    self.actionCompute.setText("Compute New Variable")
    self.actionCompute.triggered.connect(self.show_compute_variable_dialog_lazy)
    self.toolBar.addAction(self.actionCompute)
    
    # Shortcuts for "Go to Start/End Row/Column"
    self.go_to_start_row_action = QAction(self)
    self.go_to_start_row_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Up))
    self.go_to_start_row_action.triggered.connect(lambda : go_to_start_row(self))
    self.addAction(self.go_to_start_row_action)

    self.go_to_end_row_action = QAction(self)
    self.go_to_end_row_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Down))
    self.go_to_end_row_action.triggered.connect(lambda : go_to_end_row(self))
    self.addAction(self.go_to_end_row_action)

    self.go_to_start_column_action = QAction(self)
    self.go_to_start_column_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Left))
    self.go_to_start_column_action.triggered.connect(lambda : go_to_start_column(self))
    self.addAction(self.go_to_start_column_action)

    self.go_to_end_column_action = QAction(self)
    self.go_to_end_column_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Right))
    self.go_to_end_column_action.triggered.connect(lambda : go_to_end_column(self))
    self.addAction(self.go_to_end_column_action)

    # Add spacer to push following items to the right
    spacer = QWidget(self)
    spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
    self.toolBar.addWidget(spacer)

    # Add "Setting" button to the right
    self.actionSetting = QAction(self)
    icon_setting = QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'setting.svg'))
    self.actionSetting.setIcon(icon_setting)
    self.actionSetting.setText("Setting")
    self.actionSetting.triggered.connect(self.change_font_size)
    self.toolBar.addAction(self.actionSetting)
    

    # Menu "Settings"
    menu_settings = self.menu_bar.addMenu("Settings")
    action_change_font_size = QAction("Change Font Size", self)
    action_change_font_size.setIcon(QIcon(os.path.join(os.path.dirname(__file__), '..', 'assets', 'setting.svg')))
    action_change_font_size.triggered.connect(self.change_font_size)
    menu_settings.addAction(action_change_font_size)

    # Menetapkan ukuran default
    self.resize(800, 600)

Initialize the user interface of the main window. This method sets up the main layout, including a splitter, tab widgets, and various tabs for data editing, data output, and other functionalities. It also configures the menu bar with different menus and actions, and sets up the toolbar with various actions and icons.

Tabs

  • Data Editor: Allows editing of data in a spreadsheet format.
  • Data Output: Displays output data in a table view.
  • Output: Displays output in a scrollable area.
  • File: Contains actions to load and save files.
  • Exploration: Contains actions for data exploration such as summary data, normality test, correlation, etc.
  • Graph: Contains actions to generate different types of plots.
  • Model: Contains actions related to different modeling techniques.
  • Compute: Contains actions to compute new variables.
  • About: Contains information about the application.
  • Settings: Contains actions to change application settings.

Toolbar

  • Load File: Action to load a file.
  • Save Data: Action to save data.
  • Undo: Action to undo the last operation.
  • Redo: Action to redo the last undone operation.
  • Compute New Variable: Action to compute a new variable.
  • Setting: Action to open settings.

Shortcuts

  • Go to Start Row: Ctrl + Up
  • Go to End Row: Ctrl + Down
  • Go to Start Column: Ctrl + Left
  • Go to End Column: Ctrl + Right

Layout

  • The main layout is a vertical box layout containing the tab widget.
  • The central widget is set with this layout.
  • The main window is resized to a default size of 800x600.
def keyPressEvent(self, event)
Expand source code
def keyPressEvent(self, event):
    """Handle keyboard shortcuts for copy, paste, undo, and redo."""
    if event.matches(QKeySequence.StandardKey.Copy):
        self.copy_selection()
    elif event.matches(QKeySequence.StandardKey.Paste):
        self.paste_selection()
    elif event.matches(QKeySequence.StandardKey.Undo):
        self.undo_action()
    elif event.matches(QKeySequence.StandardKey.Redo):
        self.redo_action()
    else:
        super().keyPressEvent(event)

Handle keyboard shortcuts for copy, paste, undo, and redo.

def load_stylesheet_with_font_size(self, size)
Expand source code
def load_stylesheet_with_font_size(self, size):
    """
    Memuat stylesheet dan mengganti ukuran font global.
    """
    stylesheet_path = os.path.join(self.path, 'assets', 'style', 'style.qss')
    if os.path.exists(stylesheet_path):
        with open(stylesheet_path, 'r') as file:
            stylesheet = file.read()
            # stylesheet = stylesheet.replace("font-size: 12px;", f"font-size: {size}px;")
            stylesheet = stylesheet.replace("__FONT_SIZE__", f"{size}px")
            return stylesheet
    else:
        print(f"Stylesheet tidak ditemukan di {stylesheet_path}")
        return ""

Memuat stylesheet dan mengganti ukuran font global.

def load_temp_data(self)
Expand source code
def load_temp_data(self):
    """
    Load data1 and data2 from parquet, and output from JSON, if they exist.
    """
    app_path = os.path.join(os.getenv("APPDATA"), "saePisan")
    temp_dir = os.path.join(app_path, 'file-data')
    data1_path = os.path.join(temp_dir, 'sae_pisan_data1.parquet')
    data2_path = os.path.join(temp_dir, 'sae_pisan_data2.parquet')
    output_path = os.path.join(temp_dir, 'sae_pisan_output.json')
    if os.path.exists(data1_path) and os.path.exists(data2_path) and os.path.exists(output_path):
        reply = QMessageBox.question(self, 'Load Temporary Data',
                                     'Temporary data was found. Do you want to load it?',
                                     QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                     QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            self.data1 = pl.read_parquet(data1_path)
            self.data2 = pl.read_parquet(data2_path)
            self.model1.set_data(self.data1)
            self.model2.set_data(self.data2)
            self.update_table(1, self.model1)
            self.update_table(2, self.model2)
            with open(output_path, 'r') as file:
                data = json.load(file)
                self.set_output_data(data.get('output', []), timestamp=data.get('timestamp'))
    else:
        QMessageBox.warning(self, 'No Recent Data', 'No recent data file was found.')

Load data1 and data2 from parquet, and output from JSON, if they exist.

def open_about_dialog(self)
Expand source code
def open_about_dialog(self):
    """
    Opens the About dialog window.
    This method creates an instance of the AboutDialog class, passing the
    current instance as the parent, and then executes the dialog.
    """
    
    about_dialog = AboutDialog(self)
    about_dialog.exec()

Opens the About dialog window. This method creates an instance of the AboutDialog class, passing the current instance as the parent, and then executes the dialog.

def open_box_plot_dialog(self)
Expand source code
def open_box_plot_dialog(self):
    """
    Opens the box plot dialog and sets the models for the dialog.
    This method sets the models for the box plot dialog using `self.model1` and `self.model2`,
    and then displays the dialog.
    Returns:
        None
    """
    
    self.show_box_plot_dialog.set_model(self.model1, self.model2)
    self.show_box_plot_dialog.show()

Opens the box plot dialog and sets the models for the dialog. This method sets the models for the box plot dialog using self.model1 and self.model2, and then displays the dialog.

Returns

None

def open_box_plot_dialog_lazy(self)
Expand source code
def open_box_plot_dialog_lazy(self):
    """
    Lazily initializes and opens the box plot dialog.
    This method checks if the 'show_box_plot_dialog' attribute exists.
    If it does not, it initializes 'show_box_plot_dialog' with an instance
    of BoxPlotDialog. Then, it calls the method to open the box plot dialog.
    """
    
    if not hasattr(self, 'show_box_plot_dialog'):
        self.show_box_plot_dialog = BoxPlotDialog(self)
    self.open_box_plot_dialog()

Lazily initializes and opens the box plot dialog. This method checks if the 'show_box_plot_dialog' attribute exists. If it does not, it initializes 'show_box_plot_dialog' with an instance of BoxPlotDialog. Then, it calls the method to open the box plot dialog.

def open_correlation_matrix_dialog(self)
Expand source code
def open_correlation_matrix_dialog(self):
    """
    Opens the correlation matrix dialog.
    This method sets the model data for the correlation matrix dialog
    and then displays the dialog to the user.
    The models used are `self.model1` and `self.model2`.
    """
    
    self.show_correlation_matrix_dialog.set_model(self.model1, self.model2)
    self.show_correlation_matrix_dialog.show()

Opens the correlation matrix dialog. This method sets the model data for the correlation matrix dialog and then displays the dialog to the user. The models used are self.model1 and self.model2.

def open_correlation_matrix_dialog_lazy(self)
Expand source code
def open_correlation_matrix_dialog_lazy(self):
    """
    Lazily initializes and opens the correlation matrix dialog.
    This method checks if the correlation matrix dialog has already been created.
    If not, it initializes the dialog. Then, it opens the dialog.
    Attributes:
        show_correlation_matrix_dialog (CorrelationMatrixDialog): The correlation matrix dialog instance.
    """
    
    if not hasattr(self, 'show_correlation_matrix_dialog'):
        self.show_correlation_matrix_dialog = CorrelationMatrixDialog(self)
    self.open_correlation_matrix_dialog()

Lazily initializes and opens the correlation matrix dialog. This method checks if the correlation matrix dialog has already been created. If not, it initializes the dialog. Then, it opens the dialog.

Attributes

show_correlation_matrix_dialog : CorrelationMatrixDialog
The correlation matrix dialog instance.
def open_histogram_dialog(self)
Expand source code
def open_histogram_dialog(self):
    """
    Opens the histogram dialog and sets the models for it.
    This method sets the models for the histogram dialog using `self.model1` and `self.model2`,
    and then displays the histogram dialog.
    """
    
    self.show_histogram_dialog.set_model(self.model1, self.model2)
    self.show_histogram_dialog.show()

Opens the histogram dialog and sets the models for it. This method sets the models for the histogram dialog using self.model1 and self.model2, and then displays the histogram dialog.

def open_histogram_dialog_lazy(self)
Expand source code
def open_histogram_dialog_lazy(self):
    """
    Opens the histogram dialog lazily.
    This method checks if the histogram dialog has already been created.
    If not, it initializes the HistogramDialog and assigns it to the 
    'show_histogram_dialog' attribute. Then, it opens the histogram dialog.
    """
    
    if not hasattr(self, 'show_histogram_dialog'):
        self.show_histogram_dialog = HistogramDialog(self)
    self.open_histogram_dialog()

Opens the histogram dialog lazily. This method checks if the histogram dialog has already been created. If not, it initializes the HistogramDialog and assigns it to the 'show_histogram_dialog' attribute. Then, it opens the histogram dialog.

def open_line_plot_dialog(self)
Expand source code
def open_line_plot_dialog(self):
    """
    Opens the line plot dialog and sets the models for the dialog.
    This method sets the models for the line plot dialog using `self.model1` 
    and `self.model2`, and then displays the dialog.
    Returns:
        None
    """
    
    self.show_line_plot_dialog.set_model(self.model1, self.model2)
    self.show_line_plot_dialog.show()

Opens the line plot dialog and sets the models for the dialog. This method sets the models for the line plot dialog using self.model1 and self.model2, and then displays the dialog.

Returns

None

def open_line_plot_dialog_lazy(self)
Expand source code
def open_line_plot_dialog_lazy(self):
    """
    Lazily initializes and opens the line plot dialog.
    This method checks if the 'show_line_plot_dialog' attribute exists.
    If it does not, it initializes 'show_line_plot_dialog' with an instance
    of LinePlotDialog. Then, it calls the method to open the line plot dialog.
    """
    
    if not hasattr(self, 'show_line_plot_dialog'):
        self.show_line_plot_dialog = LinePlotDialog(self)
    self.open_line_plot_dialog()

Lazily initializes and opens the line plot dialog. This method checks if the 'show_line_plot_dialog' attribute exists. If it does not, it initializes 'show_line_plot_dialog' with an instance of LinePlotDialog. Then, it calls the method to open the line plot dialog.

def open_multicollinearity_dialog(self)
Expand source code
def open_multicollinearity_dialog(self):
    """
    Opens the multicollinearity dialog and sets the models for it.
    This method sets the models for the multicollinearity dialog using
    `self.model1` and `self.model2`, and then displays the dialog.
    """
    
    self.show_multicollinearity_dialog.set_model(self.model1, self.model2)
    self.show_multicollinearity_dialog.show()

Opens the multicollinearity dialog and sets the models for it. This method sets the models for the multicollinearity dialog using self.model1 and self.model2, and then displays the dialog.

def open_multicollinearity_dialog_lazy(self)
Expand source code
def open_multicollinearity_dialog_lazy(self):
    """
    Lazily initializes and opens the multicollinearity dialog.
    This method checks if the 'show_multicollinearity_dialog' attribute exists.
    If it does not, it initializes it with an instance of MulticollinearityDialog.
    Then, it opens the multicollinearity dialog.
    Returns:
        None
    """
    
    if not hasattr(self, 'show_multicollinearity_dialog'):
        self.show_multicollinearity_dialog = MulticollinearityDialog(self)
    self.open_multicollinearity_dialog()

Lazily initializes and opens the multicollinearity dialog. This method checks if the 'show_multicollinearity_dialog' attribute exists. If it does not, it initializes it with an instance of MulticollinearityDialog. Then, it opens the multicollinearity dialog.

Returns

None

def open_normality_test_dialog(self)
Expand source code
def open_normality_test_dialog(self):
    """
    Opens the normality test dialog.
    This method sets the models for the normality test dialog and then displays the dialog.
    It uses `self.model1` and `self.model2` as the models to be set in the dialog.
    Returns:
        None
    """
    
    self.show_normality_test_dialog.set_model( self.model1, self.model2)
    self.show_normality_test_dialog.show()

Opens the normality test dialog. This method sets the models for the normality test dialog and then displays the dialog. It uses self.model1 and self.model2 as the models to be set in the dialog.

Returns

None

def open_normality_test_dialog_lazy(self)
Expand source code
def open_normality_test_dialog_lazy(self):
    """
    Lazily initializes and opens the normality test dialog.
    This method checks if the normality test dialog has already been created.
    If not, it initializes the dialog and then opens it. If the dialog has 
    already been created, it simply opens the existing instance.
    """
    
    if not hasattr(self, 'show_normality_test_dialog'):
        self.show_normality_test_dialog = NormalityTestDialog(self)
    self.open_normality_test_dialog()

Lazily initializes and opens the normality test dialog. This method checks if the normality test dialog has already been created. If not, it initializes the dialog and then opens it. If the dialog has already been created, it simply opens the existing instance.

def open_scatter_plot_dialog(self)
Expand source code
def open_scatter_plot_dialog(self):
    """
    Opens the scatter plot dialog and sets the models for the dialog.
    This method sets the models for the scatter plot dialog using 
    `self.model1` and `self.model2`, and then displays the dialog.
    """
    
    self.show_scatter_plot_dialog.set_model(self.model1, self.model2)
    self.show_scatter_plot_dialog.show()

Opens the scatter plot dialog and sets the models for the dialog. This method sets the models for the scatter plot dialog using self.model1 and self.model2, and then displays the dialog.

def open_scatter_plot_dialog_lazy(self)
Expand source code
def open_scatter_plot_dialog_lazy(self):
    """
    Lazily initializes and opens the scatter plot dialog.
    This method checks if the scatter plot dialog has already been created.
    If not, it initializes the dialog and then opens it. This ensures that
    the dialog is only created when needed, potentially saving resources.
    Attributes:
        show_scatter_plot_dialog (ScatterPlotDialog): The scatter plot dialog instance.
    """
    
    if not hasattr(self, 'show_scatter_plot_dialog'):
        self.show_scatter_plot_dialog = ScatterPlotDialog(self)
    self.open_scatter_plot_dialog()

Lazily initializes and opens the scatter plot dialog. This method checks if the scatter plot dialog has already been created. If not, it initializes the dialog and then opens it. This ensures that the dialog is only created when needed, potentially saving resources.

Attributes

show_scatter_plot_dialog : ScatterPlotDialog
The scatter plot dialog instance.
def open_summary_data_dialog(self)
Expand source code
def open_summary_data_dialog(self):
    """
    Opens the summary data dialog.
    This method sets the models for the summary data dialog and then displays the dialog.
    It uses `self.model1` and `self.model2` as the models to be set in the dialog.
    Returns:
        None
    """
    
    self.show_summary_data_dialog.set_model(self.model1, self.model2)
    self.show_summary_data_dialog.show()

Opens the summary data dialog. This method sets the models for the summary data dialog and then displays the dialog. It uses self.model1 and self.model2 as the models to be set in the dialog.

Returns

None

def open_summary_data_dialog_lazy(self)
Expand source code
def open_summary_data_dialog_lazy(self):
    """
    Lazily initializes and opens the summary data dialog.
    This method checks if the 'show_summary_data_dialog' attribute exists.
    If it does not exist, it initializes it with an instance of SummaryDataDialog.
    Then, it calls the 'open_summary_data_dialog' method to open the dialog.
    """
    
    if not hasattr(self, 'show_summary_data_dialog'):
        self.show_summary_data_dialog = SummaryDataDialog(self)
    self.open_summary_data_dialog()

Lazily initializes and opens the summary data dialog. This method checks if the 'show_summary_data_dialog' attribute exists. If it does not exist, it initializes it with an instance of SummaryDataDialog. Then, it calls the 'open_summary_data_dialog' method to open the dialog.

def open_variable_selection_dialog(self)
Expand source code
def open_variable_selection_dialog(self):
    """
    Opens the variable selection dialog.
    This method sets the model for the variable selection dialog using 
    `self.model1` and `self.model2`, and then displays the dialog.
    """
    
    self.show_variable_selection_dialog.set_model(self.model1, self.model2)
    self.show_variable_selection_dialog.show()

Opens the variable selection dialog. This method sets the model for the variable selection dialog using self.model1 and self.model2, and then displays the dialog.

def open_variable_selection_dialog_lazy(self)
Expand source code
def open_variable_selection_dialog_lazy(self):
    """
    Lazily initializes and opens the variable selection dialog.
    This method checks if the 'show_variable_selection_dialog' attribute
    exists. If it does not, it initializes it with an instance of 
    VariableSelectionDialog. Then, it calls the method to open the 
    variable selection dialog.
    """
    
    if not hasattr(self, 'show_variable_selection_dialog'):
        self.show_variable_selection_dialog = VariableSelectionDialog(self)
    self.open_variable_selection_dialog()

Lazily initializes and opens the variable selection dialog. This method checks if the 'show_variable_selection_dialog' attribute exists. If it does not, it initializes it with an instance of VariableSelectionDialog. Then, it calls the method to open the variable selection dialog.

def paste_selection(self)
Expand source code
def paste_selection(self):
    """Paste clipboard content to selected cells."""
    clipboard = QApplication.clipboard()
    data = clipboard.text().split('\n')
    selection = self.spreadsheet.selectionModel().selectedIndexes()
    if selection:
        start_row = selection[0].row()
        start_col = selection[0].column()
        for i, row in enumerate(data):
            for j, value in enumerate(row.split('\t')):
                index = self.model1.index(start_row + i, start_col + j)
                self.model1.setData(index, value, Qt.ItemDataRole.EditRole)

Paste clipboard content to selected cells.

def redo_action(self)
Expand source code
def redo_action(self):
    """Redo the last undone action."""
    self.model1.redo()

Redo the last undone action.

def remove_output(self, card_frame)
Expand source code
def remove_output(self, card_frame):
    """Menghapus output dari layout"""
    self.output_layout.removeWidget(card_frame)
    card_frame.deleteLater()

    # Hapus spacer jika masih ada widget lain
    if self.output_layout.count() > 0:
        last_item = self.output_layout.itemAt(self.output_layout.count() - 1)
        if isinstance(last_item.spacerItem(), QSpacerItem):
            self.output_layout.removeItem(last_item)

    # Tambahkan stretch hanya jika tidak ada output tersisa
    if self.output_layout.count() == 0:
        self.output_layout.addStretch()

Menghapus output dari layout

def rename_column(self, column_index)
Expand source code
def rename_column(self, column_index):
    """Rename the column at the given index."""
    current_name = self.model1.headerData(column_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole)
    new_name, ok = QInputDialog.getText(self, "Rename Column", "New column name:", text=current_name)
    if ok and new_name:
        self.model1.rename_column(column_index, new_name)
        self.update_table(1, self.model1)

Rename the column at the given index.

def set_font_size(self, size)
Expand source code
def set_font_size(self, size):
    """
    Sets the font size for the main window.
    Args:
        size (int): The desired font size to be set.
    This method updates the stylesheet of the main window with the specified font size.
    """
    
    stylesheet = self.load_stylesheet_with_font_size(size)
    self.setStyleSheet(stylesheet)

Sets the font size for the main window.

Args

size : int
The desired font size to be set.

This method updates the stylesheet of the main window with the specified font size.

def set_output_data(parent, output_data, timestamp=None)
Expand source code
def set_output_data(parent, output_data, timestamp=None):
    """
    Menampilkan kembali output card dari data hasil get_output_data.
    """
    for output in output_data:
        r_script = output.get("r_script", "")
        results = output.get("result", "")
        for key, value in results.items():
            if isinstance(value, dict):
                df = pl.DataFrame(value)
                results[key] = df
            else:
                results[key] = value
        if "Plot" in results:
            display_script_and_output(parent, r_script, results, plot_paths=results["Plot"], timestamps=timestamp)
        else:
            display_script_and_output(parent, r_script, results, plot_paths=None, timestamps=timestamp)

Menampilkan kembali output card dari data hasil get_output_data.

def set_path(self, path)
Expand source code
def set_path(self, path):
    """
    Sets the path attribute for the MainWindow instance.
    Args:
        path (str): The path to be set.
    """
    
    self.path=path

Sets the path attribute for the MainWindow instance.

Args

path : str
The path to be set.
def show_compute_variable_dialog_lazy(self)
Expand source code
def show_compute_variable_dialog_lazy(self):
    """
    Displays the Compute Variable dialog lazily.
    This method initializes the Compute Variable dialog if it hasn't been created yet,
    sets its model to `self.model1`, and then shows the dialog.
    If `self.show_compute_variable_dialog` is already initialized, it simply updates
    the model and shows the dialog.
    Returns:
        None
    """
    
    if self.show_compute_variable_dialog is None:
        self.show_compute_variable_dialog = ComputeVariableDialog(self)
    self.show_compute_variable_dialog.set_model(self.model1)
    self.show_compute_variable_dialog.show()

Displays the Compute Variable dialog lazily. This method initializes the Compute Variable dialog if it hasn't been created yet, sets its model to self.model1, and then shows the dialog. If self.show_compute_variable_dialog is already initialized, it simply updates the model and shows the dialog.

Returns

None

def show_context_menu(self, pos, card_frame)
Expand source code
def show_context_menu(self, pos, card_frame):
    """Menampilkan menu klik kanan di setiap output"""
    menu = QMenu(self)
    delete_action = menu.addAction("Hapus Output")
    copy_image_action = menu.addAction("Copy Output Image")
    action = menu.exec(card_frame.mapToGlobal(pos))

    if action == delete_action:
        self.remove_output(card_frame)
    elif action == copy_image_action:
        self.copy_output_image(card_frame)

Menampilkan menu klik kanan di setiap output

def show_header_context_menu(self, pos)
Expand source code
def show_header_context_menu(self, pos):
    """Show context menu for header."""
    header = self.spreadsheet.horizontalHeader()
    logical_index = header.logicalIndexAt(pos)
    menu = QMenu(self)
    rename_action = QAction("Rename Column", self)
    rename_action.triggered.connect(lambda: self.rename_column(logical_index))
    menu.addAction(rename_action)
    
    edit_type_action = QAction("Edit Data Type", self)
    edit_type_action.triggered.connect(lambda: self.edit_data_type(logical_index))
    menu.addAction(edit_type_action)
    
    selection = self.spreadsheet.selectionModel().selectedIndexes()
    has_selection = bool(selection)
    
    delete_column_action = QAction("Delete Column", self)
    delete_column_action.triggered.connect(lambda : confirm_delete_selected_columns(self))
    delete_column_action.setEnabled(has_selection)
    menu.addAction(delete_column_action)
    
    add_column_before_action = QAction("Add Column Before", self)
    add_column_before_action.triggered.connect(lambda: show_add_column_before_dialog(self))
    add_column_before_action.setEnabled(has_selection)
    menu.addAction(add_column_before_action)
    
    add_column_after_action = QAction("Add Column After", self)
    add_column_after_action.triggered.connect(lambda: show_add_column_after_dialog(self))
    add_column_after_action.setEnabled(has_selection)
    menu.addAction(add_column_after_action)
    
    menu.exec(header.mapToGlobal(pos))

Show context menu for header.

def show_header_icon_info(self)
Expand source code
def show_header_icon_info(self):
    """
    Show a dialog explaining the meaning of header icons in the data table.
    """
    msg = (
        "<b>Header Icon Legend:</b><br><br>"
        "<div style='display:flex; flex-direction:column; gap:8px;'>"
        "<div><img src='assets/nominal.svg' width='24' height='24' style='vertical-align:middle;'> <b>Nominal/String</b></div>"
        "<div><img src='assets/null.svg' width='24' height='24' style='vertical-align:middle;'> <b>Null/Empty</b></div>"
        "<div><img src='assets/numeric.svg' width='24' height='24' style='vertical-align:middle;'> <b>Numeric</b></div>"
        "</div><br>"
        "<div style='max-width:350px;'>"
        "These icons indicate the data type of each column in the table. "
        "Nominal/String columns are represented by the nominal icon, "
        "Null/Empty columns are represented by the null icon, and "
        "Numeric columns are represented by the numeric icon."
        "</div>"
    )
    QMessageBox.information(self, "Header Icon Info", msg)

Show a dialog explaining the meaning of header icons in the data table.

def show_modeling_saeHB_dialog_lazy(self)
Expand source code
def show_modeling_saeHB_dialog_lazy(self):
    """
    Displays the ModelingSaeHBDialog lazily.
    This method initializes the ModelingSaeHBDialog if it has not been created yet,
    sets its model to `self.model1`, and then shows the dialog.
    Attributes:
        show_modeling_saeHB_dialog (ModelingSaeHBDialog): The dialog instance to be shown.
        model1: The model to be set in the dialog.
    """
    
    if self.show_modeling_saeHB_dialog is None:
        self.show_modeling_saeHB_dialog = ModelingSaeHBDialog(self)
    self.show_modeling_saeHB_dialog.set_model(self.model1)
    self.show_modeling_saeHB_dialog.show()

Displays the ModelingSaeHBDialog lazily. This method initializes the ModelingSaeHBDialog if it has not been created yet, sets its model to self.model1, and then shows the dialog.

Attributes

show_modeling_saeHB_dialog : ModelingSaeHBDialog
The dialog instance to be shown.
model1
The model to be set in the dialog.
def show_modeling_saeHB_normal_dialog_lazy(self)
Expand source code
def show_modeling_saeHB_normal_dialog_lazy(self):
    """
    Lazily initializes and displays the ModelingSaeHBNormalDialog.
    If the dialog has not been created yet, it initializes a new instance
    of ModelingSaeHBNormalDialog and sets its model to self.model1. 
    Then, it shows the dialog.
    Attributes:
        show_modeling_saeHB_normal_dialog (ModelingSaeHBNormalDialog): 
        The dialog instance to be displayed.
        model1: The model to be set in the dialog.
    """
    
    if self.show_modeling_saeHB_normal_dialog is None:
        self.show_modeling_saeHB_normal_dialog = ModelingSaeHBNormalDialog(self)
    self.show_modeling_saeHB_normal_dialog.set_model(self.model1)
    self.show_modeling_saeHB_normal_dialog.show()

Lazily initializes and displays the ModelingSaeHBNormalDialog. If the dialog has not been created yet, it initializes a new instance of ModelingSaeHBNormalDialog and sets its model to self.model1. Then, it shows the dialog.

Attributes

show_modeling_saeHB_normal_dialog : ModelingSaeHBNormalDialog
 
The dialog instance to be displayed.
model1
The model to be set in the dialog.
def show_modeling_sae_dialog_lazy(self)
Expand source code
def show_modeling_sae_dialog_lazy(self):
    """
    Lazily initializes and displays the ModelingSaeDialog.
    If the ModelingSaeDialog has not been created yet, this method will
    instantiate it and set its model to `self.model1`. Then, it will
    display the dialog.
    Returns:
        None
    """
    
    if self.show_modeling_sae_dialog is None:
        self.show_modeling_sae_dialog = ModelingSaeDialog(self)
    self.show_modeling_sae_dialog.set_model(self.model1)
    self.show_modeling_sae_dialog.show()

Lazily initializes and displays the ModelingSaeDialog. If the ModelingSaeDialog has not been created yet, this method will instantiate it and set its model to self.model1. Then, it will display the dialog.

Returns

None

def show_modeling_sae_unit_dialog_lazy(self)
Expand source code
def show_modeling_sae_unit_dialog_lazy(self):
    """
    Lazily initializes and displays the ModelingSaeUnitDialog.
    If the dialog has not been created yet, it initializes it with the current instance.
    Then, it sets the model for the dialog and shows it.
    Attributes:
        show_modeling_sae_unit_dialog (ModelingSaeUnitDialog): The dialog for modeling SAE units.
        model1: The model to be set in the dialog.
    """
    
    if self.show_modeling_sae_unit_dialog is None:
        self.show_modeling_sae_unit_dialog = ModelingSaeUnitDialog(self)
    self.show_modeling_sae_unit_dialog.set_model(self.model1)
    self.show_modeling_sae_unit_dialog.show()

Lazily initializes and displays the ModelingSaeUnitDialog. If the dialog has not been created yet, it initializes it with the current instance. Then, it sets the model for the dialog and shows it.

Attributes

show_modeling_sae_unit_dialog : ModelingSaeUnitDialog
The dialog for modeling SAE units.
model1
The model to be set in the dialog.
def show_modellig_sae_pseudo_dialog_lazy(self)
Expand source code
def show_modellig_sae_pseudo_dialog_lazy(self):
    """
    Lazily initializes and displays the Modeling SAE Pseudo dialog.
    This method checks if the `show_modellig_sae_pseudo_dialog` attribute is None.
    If it is, it initializes a new instance of `ModelingSaePseudoDialog` with the current
    instance (`self`) as the parent. It then sets the model for the dialog using `self.model1`
    and displays the dialog.
    Returns:
        None
    """
    
    if self.show_modellig_sae_pseudo_dialog is None:
        self.show_modellig_sae_pseudo_dialog = ModelingSaePseudoDialog(self)
    self.show_modellig_sae_pseudo_dialog.set_model(self.model1)
    self.show_modellig_sae_pseudo_dialog.show()

Lazily initializes and displays the Modeling SAE Pseudo dialog. This method checks if the show_modellig_sae_pseudo_dialog attribute is None. If it is, it initializes a new instance of ModelingSaePseudoDialog with the current instance (self) as the parent. It then sets the model for the dialog using self.model1 and displays the dialog.

Returns

None

def show_output(self, title, content)
Expand source code
def show_output(self, title, content):
    """Display output in the Output tab"""
    label = QLabel(content)
    self.output_layout.addWidget(label)
    self.output_tab_widget.setCurrentIndex(0)

Display output in the Output tab

def show_projection_variabel_dialog_lazy(self)
Expand source code
def show_projection_variabel_dialog_lazy(self):
    """
    Lazily initializes and displays the ProjectionDialog.
    This method checks if the `show_projection_variabel_dialog` attribute is None.
    If it is, it initializes it with a new instance of `ProjectionDialog`.
    It then sets the model for the dialog and checks if the prerequisites for
    showing the dialog are met. If they are, it displays the dialog.
    Attributes:
        show_projection_variabel_dialog (ProjectionDialog): The dialog to be shown.
        model1: The model to be set in the dialog.
    Returns:
        None
    """
    
    if self.show_projection_variabel_dialog is None:
        self.show_projection_variabel_dialog = ProjectionDialog(self)
    self.show_projection_variabel_dialog.set_model(self.model1)
    if self.show_projection_variabel_dialog.show_prerequisites():
        self.show_projection_variabel_dialog.show()

Lazily initializes and displays the ProjectionDialog. This method checks if the show_projection_variabel_dialog attribute is None. If it is, it initializes it with a new instance of ProjectionDialog. It then sets the model for the dialog and checks if the prerequisites for showing the dialog are met. If they are, it displays the dialog.

Attributes

show_projection_variabel_dialog : ProjectionDialog
The dialog to be shown.
model1
The model to be set in the dialog.

Returns

None

def show_r_packages_info(self)
Expand source code
def show_r_packages_info(self):
    """
    Show a dialog listing the R packages used and their versions in a single table with 6 columns (3 pairs of Package/Version side by side).
    """
    try:
        import rpy2.robjects as ro
        packages = [
            "sae", "arrow", "sae.projection", "emdi", "xgboost", "LiblineaR",
            "kernlab", "GGally", "ggplot2", "ggcorrplot", "car", "nortest",
            "tidyr", "carData", "dplyr", "tseries", "FSelector", "rjags", "saeHB"
        ]
        versions = []
        for pkg in packages:
            try:
                ver = ro.r(f"as.character(packageVersion('{pkg}'))")[0]
            except Exception:
                ver = "Not Installed"
            versions.append((pkg, ver))

        # Arrange into 3 columns (each column is Package/Version pair)
        n_cols = 3
        n_rows = (len(versions) + n_cols - 1) // n_cols
        table_cells = []
        for i in range(n_rows):
            row = []
            for j in range(n_cols):
                idx = i + j * n_rows
                if idx < len(versions):
                    row.extend(versions[idx])
                else:
                    row.extend(("", ""))
            table_cells.append(row)

        msg = "<b>R Packages Used:</b><br><br>"
        msg += "<table style='border-collapse:collapse;font-size:13px;'>"
        # Header
        msg += "<tr style='background-color:#f2f2f2;'>"
        for i in range(n_cols):
            msg += "<th style='border:1px solid #bbb; padding:6px 16px;'>Package</th>"
            msg += "<th style='border:1px solid #bbb; padding:6px 16px;'>Version</th>"
        msg += "</tr>"
        # Rows
        for row in table_cells:
            msg += "<tr>"
            for cell in row:
                msg += f"<td style='border:1px solid #bbb; padding:6px 16px;'>{cell}</td>"
            msg += "</tr>"
        msg += "</table>"
    except Exception as e:
        msg = f"Could not retrieve R package versions.<br>Error: {e}"
    QMessageBox.information(self, "R Packages Used", msg)

Show a dialog listing the R packages used and their versions in a single table with 6 columns (3 pairs of Package/Version side by side).

def show_toast(self)
Expand source code
def show_toast(self):
    toast = CustomToast(
        parent=self,
        title="Saved",
        text="Data, Data Output, and Output was saved",
        duration=3000,
        position="top-right"
    )
    toast.set_border_radius(8)
    toast.show()
def undo_action(self)
Expand source code
def undo_action(self):
    """Undo the last action."""
    self.model1.undo()

Undo the last action.

def update_table(self, sheet_number, model)
Expand source code
def update_table(self, sheet_number, model):
    """Memperbarui tabel pada sheet tertentu dengan model baru"""
    if sheet_number == 1:
        self.spreadsheet.setModel(model)
        self.model1 = model
        self.spreadsheet.resizeColumnsToContents()
        self.tab_widget.setCurrentWidget(self.tab1)
        if self.show_modeling_sae_dialog:
            self.show_modeling_sae_dialog.set_model(model)
        if self.show_modeling_saeHB_dialog:
            self.show_modeling_saeHB_dialog.set_model(model)
        if self.show_modeling_sae_unit_dialog:
            self.show_modeling_sae_unit_dialog.set_model(model)
        if self.show_modeling_saeHB_normal_dialog:
            self.show_modeling_saeHB_normal_dialog.set_model(model)
        if self.show_modellig_sae_pseudo_dialog:
            self.show_modellig_sae_pseudo_dialog.set_model(model)
        if self.show_compute_variable_dialog:
            self.show_compute_variable_dialog.set_model(model)
        if self.show_projection_variabel_dialog:
            self.show_projection_variabel_dialog.set_model(model)
    elif sheet_number == 2:
        self.table_view2.setModel(model)
        self.model2 = model
        self.table_view2.resizeColumnsToContents()

Memperbarui tabel pada sheet tertentu dengan model baru