ragardner / tksheet

Python tkinter table widget for displaying tabular data
https://pypi.org/project/tksheet/
MIT License
400 stars 48 forks source link

Copy&Paste #232

Closed kenro1980 closed 3 months ago

kenro1980 commented 3 months ago

I feel like copying and pasting becomes strange when I select from the bottom right to the top left. Is this by design?

Below is what I have modified.

def ctrl_v(self, event: object = None, validation: bool = True) -> None | EventDataDict:
    if not self.PAR.ops.paste_can_expand_x and len(self.col_positions) == 1:
        return
    if not self.PAR.ops.paste_can_expand_y and len(self.row_positions) == 1:
        return
    event_data = event_dict(
        name="edit_table",
        sheet=self.PAR.name,
        widget=self,
        selected=self.selected,
    )
    if self.selected:
        selected_r = self.selected.row
        selected_c = self.selected.column
    elif not self.selected and not self.PAR.ops.paste_can_expand_x and not self.PAR.ops.paste_can_expand_y:
        return
    else:
        if not self.data:
            selected_c, selected_r = 0, 0
        else:
            if len(self.col_positions) == 1 and len(self.row_positions) > 1:
                selected_c, selected_r = 0, len(self.row_positions) - 1
            elif len(self.row_positions) == 1 and len(self.col_positions) > 1:
                selected_c, selected_r = len(self.col_positions) - 1, 0
            elif len(self.row_positions) > 1 and len(self.col_positions) > 1:
                selected_c, selected_r = 0, len(self.row_positions) - 1
    try:
        data = get_data_from_clipboard(
            widget=self,
            delimiters=self.PAR.ops.from_clipboard_delimiters,
            lineterminator=self.PAR.ops.to_clipboard_lineterminator,
        )
    except Exception:
        return
    new_data_numcols = max(map(len, data))
    new_data_numrows = len(data)
    for rn, r in enumerate(data):
        if len(r) < new_data_numcols:
            data[rn] += list(repeat("", new_data_numcols - len(r)))
    if self.selected:
        (
            lastbox_r1,
            lastbox_c1,
            lastbox_r2,
            lastbox_c2,
        ) = self.selection_boxes[self.selected.fill_iid].coords
        #-------------------------------------------------------------
        # Old process
        #-------------------------------------------------------------
        '''
        lastbox_numrows = lastbox_r2 - lastbox_r1
        lastbox_numcols = lastbox_c2 - lastbox_c1
        if lastbox_numrows > new_data_numrows and not lastbox_numrows % new_data_numrows:
            nd = []
            for _ in range(int(lastbox_numrows / new_data_numrows)):
                nd.extend(r.copy() for r in data)
            data.extend(nd)
            new_data_numrows *= int(lastbox_numrows / new_data_numrows)
        if lastbox_numcols > new_data_numcols and not lastbox_numcols % new_data_numcols:
            for rn, r in enumerate(data):
                for _ in range(int(lastbox_numcols / new_data_numcols)):
                    data[rn].extend(r.copy())
            new_data_numcols *= int(lastbox_numcols / new_data_numcols)
        '''
        #-------------------------------------------------------------
        # 2024/06/05 Fixed paste selection
        #-------------------------------------------------------------
        if selected_r == lastbox_r1:
            selected_r_rev = False # Row Reversal flag
        else: # selection from bottom to top
            selected_r_rev = True # Row Reversal flag
            selected_r = lastbox_r1
        if selected_c == lastbox_c1:
            selected_c_rev = False # Col Reversal flag
        else: # selection from right to left
            selected_c_rev = True # Col Reversal flag
            selected_c = lastbox_c1
        lastbox_numrows = lastbox_r2 - lastbox_r1
        lastbox_numcols = lastbox_c2 - lastbox_c1
        if lastbox_numrows > new_data_numrows:
            nd = []
            data_r_len = int(lastbox_numrows / new_data_numrows) - 1
            for _ in range(data_r_len):
                nd.extend(r.copy() for r in data)
            ext_row = lastbox_numrows % new_data_numrows # surplus
            for i in range(ext_row):
                nd.append(data[i].copy())
            data.extend(nd)
            new_data_numrows *= int(lastbox_numrows / new_data_numrows)
            new_data_numrows += ext_row # partial data

        if lastbox_numcols > new_data_numcols:
            data_c_len = int(lastbox_numcols / new_data_numcols) - 1
            for rn, r in enumerate(data):
                nd = []
                for _ in range(data_c_len):
                    nd.extend(r.copy())
                data[rn].extend(nd.copy())
            ext_col = lastbox_numcols % new_data_numcols # surplus
            if 0 != ext_col:
                for rn, r in enumerate(data): # partial data
                    nd.extend(r[:ext_col].copy())
                    data[rn].extend(r[:ext_col].copy())
            new_data_numcols *= int(lastbox_numcols / new_data_numcols)
            new_data_numcols += ext_col # partial data
        #-------------------------------------------------------------
    event_data["data"] = data
    added_rows = 0
    added_cols = 0
    total_data_cols = None
    if self.PAR.ops.paste_can_expand_x:
        if selected_c + new_data_numcols > len(self.col_positions) - 1:
            total_data_cols = self.equalize_data_row_lengths()
            added_cols = selected_c + new_data_numcols - len(self.col_positions) + 1
            if (
                isinstance(self.PAR.ops.paste_insert_column_limit, int)
                and self.PAR.ops.paste_insert_column_limit < len(self.col_positions) - 1 + added_cols
            ):
                added_cols = self.PAR.ops.paste_insert_column_limit - len(self.col_positions) - 1
    if self.PAR.ops.paste_can_expand_y:
        if selected_r + new_data_numrows > len(self.row_positions) - 1:
            added_rows = selected_r + new_data_numrows - len(self.row_positions) + 1
            if (
                isinstance(self.PAR.ops.paste_insert_row_limit, int)
                and self.PAR.ops.paste_insert_row_limit < len(self.row_positions) - 1 + added_rows
            ):
                added_rows = self.PAR.ops.paste_insert_row_limit - len(self.row_positions) - 1
    if selected_c + new_data_numcols > len(self.col_positions) - 1:
        adjusted_new_data_numcols = len(self.col_positions) - 1 - selected_c
    else:
        adjusted_new_data_numcols = new_data_numcols
    if selected_r + new_data_numrows > len(self.row_positions) - 1:
        adjusted_new_data_numrows = len(self.row_positions) - 1 - selected_r
    else:
        adjusted_new_data_numrows = new_data_numrows
    selected_r_adjusted_new_data_numrows = selected_r + adjusted_new_data_numrows
    selected_c_adjusted_new_data_numcols = selected_c + adjusted_new_data_numcols
    endrow = selected_r_adjusted_new_data_numrows
    #-------------------------------------------------------------
    # Old process
    #-------------------------------------------------------------
    '''
    boxes = {
        (
            selected_r,
            selected_c,
            selected_r_adjusted_new_data_numrows,
            selected_c_adjusted_new_data_numcols,
        ): "cells"
    }
    '''
    #-------------------------------------------------------------
    # 2024/06/05 Fixed paste selection
    #-------------------------------------------------------------
    sel_box_r1 = selected_r
    sel_box_r2 = selected_r_adjusted_new_data_numrows
    sel_box_c1 = selected_c
    sel_box_c2 = selected_c_adjusted_new_data_numcols
    if selected_r_rev:
        sel_r1 = selected_r
        sel_r2 = selected_r_adjusted_new_data_numrows - 1
        see_r = sel_r2
    else:
        sel_r1 = selected_r
        sel_r2 = selected_r_adjusted_new_data_numrows
        see_r = sel_r1

    if selected_c_rev:
        sel_c1 = selected_c
        sel_c2 = selected_c_adjusted_new_data_numcols - 1
        see_c = sel_c2
    else:
        sel_c1 = selected_c
        sel_c2 = selected_c_adjusted_new_data_numcols
        see_c = sel_c1

    boxes = {
        (
            sel_r1,
            sel_c1,
            sel_r2,
            sel_c2,
        ): "cells"
    }
    #-------------------------------------------------------------
    event_data["selection_boxes"] = boxes
    if not try_binding(self.extra_begin_ctrl_v_func, event_data, "begin_ctrl_v"):
        return
    # the order of actions here is important:
    # edit existing sheet (not including any added rows/columns)

    # then if there are any added rows/columns:
    # create empty rows/columns dicts for any added rows/columns
    # edit those dicts with so far unused cells of data from clipboard
    # instead of editing table using set cell data, add any new rows then columns with pasted data
    for ndr, r in enumerate(range(selected_r, selected_r_adjusted_new_data_numrows)):
        for ndc, c in enumerate(range(selected_c, selected_c_adjusted_new_data_numcols)):
            val = data[ndr][ndc]
            if (
                not self.edit_validation_func
                or not validation
                or (
                    self.edit_validation_func
                    and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None
                )
            ):
                event_data = self.event_data_set_cell(
                    datarn=self.datarn(r),
                    datacn=self.datacn(c),
                    value=val,
                    event_data=event_data,
                )
    if added_rows:
        ctr = 0
        data_ins_row = len(self.data)
        displayed_ins_row = len(self.row_positions) - 1
        if total_data_cols is None:
            total_data_cols = self.total_data_cols()
        rows, index, row_heights = self.get_args_for_add_rows(
            data_ins_row=data_ins_row,
            displayed_ins_row=displayed_ins_row,
            numrows=added_rows,
            total_data_cols=total_data_cols,
        )
        for ndr, r in zip(
            range(
                adjusted_new_data_numrows,
                new_data_numrows,
            ),
            reversed(rows),
        ):
            for ndc, c in enumerate(
                range(
                    selected_c,
                    selected_c_adjusted_new_data_numcols,
                )
            ):
                val = data[ndr][ndc]
                datacn = self.datacn(c)
                if (
                    not self.edit_validation_func
                    or not validation
                    or (
                        self.edit_validation_func
                        and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None
                        and self.input_valid_for_cell(r, datacn, val, ignore_empty=True)
                    )
                ):
                    rows[r][datacn] = val
                    ctr += 1
        if ctr:
            event_data = self.add_rows(
                rows=rows,
                index=index,
                row_heights=row_heights,
                event_data=event_data,
                mod_event_boxes=False,
            )
    if added_cols:
        ctr = 0
        if total_data_cols is None:
            total_data_cols = self.total_data_cols()
        data_ins_col = total_data_cols
        displayed_ins_col = len(self.col_positions) - 1
        columns, headers, column_widths = self.get_args_for_add_columns(
            data_ins_col=data_ins_col,
            displayed_ins_col=displayed_ins_col,
            numcols=added_cols,
        )
        # only add the extra rows if expand_y is allowed
        if self.PAR.ops.paste_can_expand_x and self.PAR.ops.paste_can_expand_y:
            endrow = selected_r + new_data_numrows
        else:
            endrow = selected_r + adjusted_new_data_numrows
        for ndr, r in enumerate(
            range(
                selected_r,
                endrow,
            )
        ):
            for ndc, c in zip(
                range(
                    adjusted_new_data_numcols,
                    new_data_numcols,
                ),
                reversed(columns),
            ):
                val = data[ndr][ndc]
                datarn = self.datarn(r)
                if (
                    not self.edit_validation_func
                    or not validation
                    or (
                        self.edit_validation_func
                        and (val := self.edit_validation_func(mod_event_val(event_data, val, (r, c)))) is not None
                        and self.input_valid_for_cell(datarn, c, val, ignore_empty=True)
                    )
                ):
                    columns[c][datarn] = val
                    ctr += 1
        if ctr:
            event_data = self.add_columns(
                columns=columns,
                header=headers,
                column_widths=column_widths,
                event_data=event_data,
                mod_event_boxes=False,
            )
    if added_rows:
        selboxr = selected_r + new_data_numrows
    else:
        selboxr = selected_r_adjusted_new_data_numrows
    if added_cols:
        selboxc = selected_c + new_data_numcols
    else:
        selboxc = selected_c_adjusted_new_data_numcols
    self.deselect("all", redraw=False)
    #-------------------------------------------------------------
    # Old process
    #-------------------------------------------------------------
    '''
    self.create_selection_box(
        selected_r,
        selected_c,
        selboxr,
        selboxc,
        "cells",
        run_binding=True,
    )
    event_data["selection_boxes"] = self.get_boxes()
    event_data["selected"] = self.selected
    if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]:
        self.undo_stack.append(pickled_event_dict(event_data))
    self.see(
        r=selected_r,
        c=selected_c,
        keep_yscroll=False,
        keep_xscroll=False,
        bottom_right_corner=False,
        check_cell_visibility=True,
        redraw=False,
    )
    '''
    #-------------------------------------------------------------
    # 2024/06/05 Fixed paste selection
    #-------------------------------------------------------------
    self.create_selection_box(
        sel_box_r1,
        sel_box_c1,
        sel_box_r2,
        sel_box_c2,
        "cells",
        set_current=(see_r, see_c),
        run_binding=True,
    )
    event_data["selection_boxes"] = self.get_boxes()
    event_data["selected"] = self.selected
    if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]:
        self.undo_stack.append(pickled_event_dict(event_data))
    self.see(
        r=see_r,
        c=see_c,
        keep_yscroll=False,
        keep_xscroll=False,
        bottom_right_corner=False,
        check_cell_visibility=True,
        redraw=False,
    )
    #-------------------------------------------------------------
    self.refresh()
    if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]:
        try_binding(self.extra_end_ctrl_v_func, event_data, "end_ctrl_v")
    self.sheet_modified(event_data)
    self.PAR.emit_event("<<Paste>>", event_data)
    return event_data
ragardner commented 3 months ago

Hello,

Thanks very much for your detailed report and comprehensive suggestion,

It wasn't intended so I really appreciate it

I have gone for a different solution in version 7.2.9, using the top left most cell as a start point, which seems to have the same behavior as a paste in google docs

But let me know if you feel it's wrong and I will re-examine the issue

Cheers

kenro1980 commented 3 months ago

It's working perfectly Thank you for correcting the code