tksheet icon indicating copy to clipboard operation
tksheet copied to clipboard

Alignment of special UTF-8 characters on labels

Open matheuskknd opened this issue 6 months ago โ€ข 8 comments

Hello again, how are you doing?

I'm not sure if I should open an issue since I basically have a question.

It turns out that we are trying to add special UTF-8 characters to the labels as in the example below, but visually the text sometimes becomes misaligned as you can see in the print. I would like to know if there is any way to make the special characters align one below the other and also to make the texts aligned to the left between the labels.

Image

Python 3.8.12 example:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import weakref
from tkinter import messagebox, simpledialog
from typing import Any, Callable, Literal, cast

import customtkinter as ctk
import tksheet


class TableFrame(ctk.CTkFrame):

	def __init__(self, master: Any, **kwargs: Any) -> None:
		super().__init__(master, **kwargs)

		# Creating the spreadsheet as an attribute
		self.sheet: tksheet.Sheet = tksheet.Sheet(
			self,
			headers=["A", "B"],
			data=[['1', '2'], ['3', '4']],
		)
		self.sheet.pack(expand=True, fill="both")
		self.sheet.enable_bindings("all")

		# Creating a highlighted readonly span
		span: tksheet.Span = self.sheet.span(0, 1, 2, 2)
		span.highlight(bg="green", fg="black", redraw=False)
		span.readonly(True)

		# Labels translation
		self.sheet.set_options(
			copy_label="๐Ÿ— Copy",
			paste_label="๐Ÿ“‹ Paste",
			edit_cell_label="โœ๏ธ Edit",
			delete_label="๐Ÿ—‘๏ธ Delete",
			cut_label="โœ„ Cut",
			sort_rows_label="โฌ‡ Sort Asc.",
			sort_rows_reverse_label="โฌ† Sort Desc.",
			delete_rows_label="๐Ÿ—‘๏ธ Delete row(s)",
			insert_rows_above_label="โž• Add 1 row above",
			insert_rows_below_label="โž• Add 1 row bellow",
			insert_row_label="โž• Insert 1 row",
			clear_contents_label="โœ–๏ธ Clear",
			copy_contents_label="๐Ÿ— Copy",
			cut_contents_label="โœ„ Cut",
			undo_label="โฎŒ Undo",
			select_all_label="โคซ Select All",
		)

		# Allows user to insert or add N rows in the sheet
		self.sheet.popup_menu_add_command(
			label="โž• Insert N row(s)",
			func=self.__create_insert_or_add_rows_command("INSERT", title="Insert row(s)",
																										button_label="Insert"),
			table_menu=False,
			index_menu=False,
			header_menu=False,
			empty_space_menu=True,
		)

		self.sheet.popup_menu_add_command(
			label="โž• Insert N row(s) above",
			func=self.__create_insert_or_add_rows_command("ADD_ABOVE", title="Add row(s) above",
																										button_label="Add above"),
			table_menu=False,
			index_menu=True,
			header_menu=False,
			empty_space_menu=False,
		)

		self.sheet.popup_menu_add_command(
			label="โž• Insert N row(s) bellow",
			func=self.__create_insert_or_add_rows_command("ADD_BELLOW", title="Add row(s) bellow",
																										button_label="Add bellow"),
			table_menu=False,
			index_menu=True,
			header_menu=False,
			empty_space_menu=False,
		)

	def __create_insert_or_add_rows_command(self, type_: Literal["ADD_ABOVE", "ADD_BELLOW", "INSERT"],
																					*, title: str, button_label: str) -> Callable[[], None]:
		w_sheet: tksheet.Sheet = weakref.proxy(self.sheet)

		def insert_or_add_rows_command() -> None:
			idx: int | None = None
			if type_.startswith("ADD"):
				selected: set[str] = cast(set[str], w_sheet.get_selected_rows())
				idx = int(next(iter(selected))) + int(type_ == "ADD_BELLOW")

			while True:
				text: str | None = simpledialog.askstring(title, button_label)
				if not text:
					return

				try:
					num_rows: int = int(text)
					if num_rows > 0:
						w_sheet.insert_rows(rows=num_rows, idx=idx, tree=False, redraw=True)
					return

				except Exception as e:
					messagebox.showwarning(f"Invalid number", str(e), parent=w_sheet)

		return insert_or_add_rows_command


def main():
	ctk.set_appearance_mode("light")
	ctk.set_default_color_theme("blue")

	root = ctk.CTk()
	root.geometry("800x400")
	root.title("customtkinter + tksheet")

	tabela = TableFrame(root)
	tabela.pack(expand=True, fill="both", padx=10, pady=10)

	root.mainloop()


if __name__ == "__main__":
	main()

Best regards

matheuskknd avatar May 02 '25 21:05 matheuskknd

Also, I searched the documentation but wasn't abble to find anything about remove certain labels/commands from certain cells. We were trying to remove the edit commands for cell also marked as readonly, sinse these commands will have no effect on them. Is there a way to do this today?

I understand that, if it still have to be implemented, then it might be easier to just make the sheet.popup_menu_add_command accept a new parameter, let's say when_readonly which defaults to "True" for backward compatibility. And then tksheet handles the items to show on the right-click for readonly cells, headers or row_indexes. What do you think?

NOTE: this last request is different from the one discussed on #281, here were talking about right-click labels on specific cells on the table, more specifically the ones marked as readonly.

matheuskknd avatar May 02 '25 21:05 matheuskknd

Hello,

Thanks for your thoughtful questions,

I don't know of any way to align special characters like the icon ones you've shown above except perhaps to use a monospace font. I am however not sure if this would be satisfactory in all cases, I think the best approach would be to use images so I made some changes to that effect.

The main changes I've made are numbered below:

  1. To address the icons and alignment in the menus I decided to add the capability to have images in menu entries added using popup_menu_add_command.
  • The new function looks like this:
popup_menu_add_command(
        label: str,
        func: Callable,
        table_menu: bool = True,
        index_menu: bool = True,
        header_menu: bool = True,
        empty_space_menu: bool = True,
        image: tk.PhotoImage | Literal[""] = "",
        compound: Literal["top", "bottom", "left", "right", "none"] | None = None,
        accelerator: str | None = None,
    ) -> Sheet
  • A drawback to this approach - the menu entries without images do not get the left hand padding that entries with images have.
  • There also seems to be a bug in tkinter, at least for me - I can't seem to find info on this, where disabling a menu item makes the image turn into a pixelated white and gray square. So I've had to remove the images of disabled menu entries, unfortunately also removing their padding in the process.
  1. I also added some changeable default free use icons to tksheet for its built in menu. You can change these icons using set_options(), I updated the documentation. This function will allow you to do it.
  • They're from this website https://lucide.dev/icons/
  • A drawback is an addition to tksheets license (updated tksheet license here). It's very similar to the MIT license: https://lucide.dev/license
  1. I added undo and redo to the built in menus, should only be visible if they're enabled.
  2. To address your query about disabling menu items, especially for readonly cells:
  • I made it the default that the Edit menu entry is disabled if the currently selected cell is readonly.
  • The undo/redo menu entries for table menus should also enable/disable appropriately.
  • I am open to considering adjusting this behavior or extending it perhaps with your suggestion of adding a parameter if you think it necessary.

Kind regards

ragardner avatar May 04 '25 16:05 ragardner

Hello! How are you doing?

First of all, thank you very much for your quick response. I have been working tirelessly to finalize the integration of tksheet in my project. Now practically all of our functionalities use it, whether for batch data entry or for displaying reports. Soon we will also use tksheet's TreeView mode to implement access management and even configuration management functionalities.

The new feature with the window in light mode looks very nice, excellent work:

Image

However, in dark mode the disabled items are quite blurry. Could you take a look, please?

Image

Sample python 3.12.8 code:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import weakref
from tkinter import messagebox, simpledialog
from typing import Any, Callable, Literal, cast

import customtkinter as ctk
import tksheet


class TableFrame(ctk.CTkFrame):

	def __init__(self, master: Any, **kwargs: Any) -> None:
		super().__init__(master, **kwargs)

		# Creating the spreadsheet as an attribute
		self.sheet: tksheet.Sheet = tksheet.Sheet(
			self,
			headers=["A", "B"],
			data=[['1', '2'], ['3', '4']],
			### Theme
			theme="dark green",
			# theme="light green",
		)
		self.sheet.pack(expand=True, fill="both")
		self.sheet.enable_bindings("all")

		# Creating a highlighted readonly span
		span: tksheet.Span = self.sheet.span(0, 1, 2, 2)
		span.highlight(bg="green", fg="black", redraw=False)
		span.readonly(True)

		# Labels translation
		self.sheet.set_options(
			copy_label="Copy",
			paste_label="Paste",
			edit_cell_label="Edit",
			delete_label="Delete",
			cut_label="Cut",
			sort_rows_label="Sort Asc.",
			sort_rows_reverse_label="Sort Desc.",
			delete_rows_label="Delete row(s)",
			insert_rows_above_label="Add 1 row above",
			insert_rows_below_label="Add 1 row bellow",
			insert_row_label="Insert 1 row",
			clear_contents_label="Clear",
			copy_contents_label="Copy",
			cut_contents_label="Cut",
			undo_label="Undo",
			select_all_label="Select All",
		)


def main():
	ctk.set_appearance_mode("light")
	ctk.set_default_color_theme("blue")

	root = ctk.CTk()
	root.geometry("800x400")
	root.title("customtkinter + tksheet")

	tabela = TableFrame(root)
	tabela.pack(expand=True, fill="both", padx=10, pady=10)

	root.mainloop()


if __name__ == "__main__":
	main()

Best regards

matheuskknd avatar May 16 '25 18:05 matheuskknd

Hello,

Thanks for letting me know how you're making use of tksheet! It's nice to hear people are finding it useful ๐Ÿ˜ƒ

About the blurring issue with the menu items in dark colors...

It seems like disabled menu items have issues on dark backgrounds with some operating systems or fonts (i'm not sure which) regardless of what I try to do about it.

I had to make it so that the menu items are built on the fly upon a right click event so that disabled items, such as edit, undo or redo are excluded if they are not necessary.

This change is in 7.5.7

Kind regards

ragardner avatar May 20 '25 19:05 ragardner

Thank you very much for the reply and the new version, @ragardner!

Hiding disabled labels is actually better, and it also solves some issues.

The only thing I think is still missing, is to hide Cut, Paste and Delete labels from read-only cells.

Image

Image

Images from the code of last example above.

Best regards

matheuskknd avatar May 25 '25 00:05 matheuskknd

Hello,

Thanks for the review of the changes,

The only thing I think is still missing, is to hide Cut, Paste and Delete labels from read-only cells.

I am not sure about how to handle this one,

The problem is that the Edit menu item check is done on a single cell and the undo and redo menu items just checks if the stacks are empty whereas cut, paste and delete I'd have to check each cell in every cell selection box to determine if they're all readonly, this could mean lag and increased battery use for larger sheets,

I could maybe only perform the check if only a single cell is selected in the entire sheet? Or perhaps under a certain size?

Kind regards

ragardner avatar Jun 03 '25 08:06 ragardner

Hi @regardner, hope you're doing well!

Sorry for the late reply โ€” various projects on my side too. ๐Ÿ˜…

Here are some thoughts on your concerns:

  1. I totally agree that performance should always be a key consideration โ€” your concern is very valid.

  2. However, since we're talking about a user-initiated right-click action, we are essentially in "user time", where slight delays tend to be more acceptable.

  3. The computational complexity of checking all selected cells is O(n). While we could optimize with an early exit (halt on the first read-only cell), the complexity remains O(n).

  4. One middle-ground approach could be to implement a configurable threshold (e.g., max cells to scan for read-only), beyond which the logic assumes no read-only cells [what would make the complexity decay to O(1) โ€” supposing the developer don't set it to infinite]. But I'm not sure if thatโ€™s the best option either.

Given all that, maybe we could stick with the current behavior for now โ€” even if slightly inconsistent โ€” as a practical trade-off. What is your opinion?

Best regards!

matheuskknd avatar Jun 13 '25 20:06 matheuskknd

Bought you more coffees!

matheuskknd avatar Jun 13 '25 21:06 matheuskknd

Good afternoon. How are you doing, @ragardner? Could you give a look into this answer above?

Best regards

matheuskknd avatar Aug 21 '25 16:08 matheuskknd

Hello,

Regarding your question, I decided to use a threshold, at the moment it is automated based on a rough estimate of how many cells could fit into the visible portion of the table.

I also tried to make significant improvements to the existing menu command filtering, it should be a lot better now. But please let me know if you have issues as a result of this change.

Kind regards

ragardner avatar Aug 26 '25 17:08 ragardner

Tested! Works like a charm. Thank you so much! ๐Ÿ’ฏ And there goes more bags to your coffee stockpile. Best regards

matheuskknd avatar Sep 15 '25 18:09 matheuskknd

Thank you for the feedback and for the coffees! Take care

ragardner avatar Sep 16 '25 08:09 ragardner