diff --git a/AUTHORS.md b/AUTHORS.md index 788a3919..b020f588 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -38,3 +38,8 @@ This file contains a list of all the authors of widgets in this repository. Plea * PR #59: Example runner (`examples/run.py`) - [jnhyperion](https://github.com/jnhyperion) * PR #31 +- [Faraaz Kurawle](https://github.com/kurawlefaraaz) + * `DynamicNotebook` + * `NumberedText`, based on idea of [yelsayed](https://stackoverflow.com/a/37087317/16187613) + * `EditableTreeview`, based on idea of [dakov](https://stackoverflow.com/a/18815802/16187613) + * [found here](https://github.com/kurawlefaraaz/Tk-Themed-Utilities) diff --git a/docs/source/authors.rst b/docs/source/authors.rst index 02ecfd6e..72ab42a8 100644 --- a/docs/source/authors.rst +++ b/docs/source/authors.rst @@ -61,3 +61,9 @@ List of all the authors of widgets in this repository. Please note that this lis - Multiple authors: * :class:`~ttkwidgets.ScaleEntry` (RedFantom and Juliette Monsel) + +- `Faraaz Kurawle `_ + + * :class:`~ttkwidgets.DynamicNotebook` + * :class:`NumberedText`, based on idea of, `yelsayed `_ + * :class:`EditableTreeview`, based on idea of, `dakov `_ diff --git a/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.DynamicNotebook.rst b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.DynamicNotebook.rst new file mode 100644 index 00000000..552dd933 --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.DynamicNotebook.rst @@ -0,0 +1,10 @@ +DynamicNotebook +===== + +.. currentmodule:: ttkwidgets + +.. autoclass:: DynamicNotebook + :show-inheritance: + :members: + + .. automethod:: __init__ \ No newline at end of file diff --git a/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.EditableTreeview.rst b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.EditableTreeview.rst new file mode 100644 index 00000000..1543c9fa --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.EditableTreeview.rst @@ -0,0 +1,10 @@ +EditableTreeview +===== + +.. currentmodule:: ttkwidgets + +.. autoclass:: EditableTreeview + :show-inheritance: + :members: + + .. automethod:: __init__ \ No newline at end of file diff --git a/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.NumberedText.rst b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.NumberedText.rst new file mode 100644 index 00000000..4d25171e --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.NumberedText.rst @@ -0,0 +1,10 @@ +NumberedText +===== + +.. currentmodule:: ttkwidgets + +.. autoclass:: NumberedText + :show-inheritance: + :members: + + .. automethod:: __init__ \ No newline at end of file diff --git a/examples/example_dynamic_notebook.py b/examples/example_dynamic_notebook.py new file mode 100644 index 00000000..e34bee2a --- /dev/null +++ b/examples/example_dynamic_notebook.py @@ -0,0 +1,11 @@ +from ttkwidgets.dynamic_notebook import DynamicNotebook +import tkinter as tk + +def demo(): + root = tk.Tk() + wksp =DynamicNotebook(root) + wksp.pack(fill="both", expand=1) + root.mainloop() + +if __name__ == "__main__": + demo() \ No newline at end of file diff --git a/examples/example_editable_treeview.py b/examples/example_editable_treeview.py new file mode 100644 index 00000000..d7b146e5 --- /dev/null +++ b/examples/example_editable_treeview.py @@ -0,0 +1,16 @@ +from ttkwidgets.editable_treeview import EditableTreeview +import tkinter as tk + +def demo(): + root = tk.Tk() + root.title("NumberedText Demo") + columns = ("attribute", "value") + data = {f"Demo {i}": f"Demo {i}" for i in range(1, 101)} + + widget = EditableTreeview(root, columns=columns, show=" tree", bind_key="", data=data) + widget.pack(expand=1, fill="both", padx=20, pady=20) + + root.mainloop() + +if __name__ == "__main__": + demo() \ No newline at end of file diff --git a/examples/example_numberedtext.py b/examples/example_numberedtext.py new file mode 100644 index 00000000..9b81a5da --- /dev/null +++ b/examples/example_numberedtext.py @@ -0,0 +1,10 @@ +from ttkwidgets.numberedtext import NumberedText +import tkinter as tk +def demo(): + root = tk.Tk() + root.title("NumberedText Demo") + NumberedText(root, bg="red").pack(side="left") + root.mainloop() + +if __name__ == "__main__": + demo() \ No newline at end of file diff --git a/tests/test_dynamicnotebook.py b/tests/test_dynamicnotebook.py new file mode 100644 index 00000000..bccfae12 --- /dev/null +++ b/tests/test_dynamicnotebook.py @@ -0,0 +1,21 @@ +# Copyright (c) FaraazKurawle 2024 +# For license see LICENSE + +from ttkwidgets.dynamic_notebook import DynamicNotebook +from tests import BaseWidgetTest + +class TestDynamicNotebook(BaseWidgetTest): + def test_dynamicnotebook_init(self): + widget = DynamicNotebook(self.window) + widget.pack() + self.window.update() + + def test_dynamicnotebook_buttons_functions(self): + widget = DynamicNotebook(self.window) + widget.pack() + widget.add_frame_button_func() + widget.remove_frame() + + def test_dynamicnotebook_kw(self): + widget = DynamicNotebook(self.window) + widget.pack() \ No newline at end of file diff --git a/tests/test_editable_treeview.py b/tests/test_editable_treeview.py new file mode 100644 index 00000000..416f0f38 --- /dev/null +++ b/tests/test_editable_treeview.py @@ -0,0 +1,13 @@ +# Copyright (c) FaraazKurawle 2024 +# For license see LICENSE + +from ttkwidgets.editable_treeview import EditableTreeview +from tests import BaseWidgetTest + +class TestEditableTreeview(BaseWidgetTest): + def test_editabletreeview_init(self): + columns = ("attribute", "value") + data = {f"Demo {i}": f"Demo {i}" for i in range(1, 101)} + + widget = EditableTreeview(self.window, columns=columns, show=" tree", bind_key="", data=data) + widget.pack(expand=1, fill="both", padx=20, pady=20) \ No newline at end of file diff --git a/tests/test_numberedtext.py b/tests/test_numberedtext.py new file mode 100644 index 00000000..6e4b1328 --- /dev/null +++ b/tests/test_numberedtext.py @@ -0,0 +1,20 @@ +# Copyright (c) FaraazKurawle 2024 +# For license see LICENSE + +from ttkwidgets.numberedtext import NumberedText +from tests import BaseWidgetTest + +class TestNumberedText(BaseWidgetTest): + def test_numberedtext_init(self): + widget = NumberedText(self.window) + widget.pack() + self.window.update() + + def test_numberedtext_buttons_functions(self): + widget = NumberedText(self.window) + widget.pack() + # No buttons + + def test_numberedtextr_kw(self): + widget = NumberedText(self.window, bg="red") + widget.pack() \ No newline at end of file diff --git a/ttkwidgets/dynamic_notebook.py b/ttkwidgets/dynamic_notebook.py new file mode 100644 index 00000000..a37cb416 --- /dev/null +++ b/ttkwidgets/dynamic_notebook.py @@ -0,0 +1,72 @@ +""" +Author: Faraaz Kurawle +License: GNU GPLv3 +Source: This repository +""" + +import tkinter as tk +import tkinter.ttk as ttk + +class DynamicNotebook(ttk.Notebook): + """ + Notebook widget with ablity to add or remove tabs in the runtime. + + :param parent: parent widget of this widget. + """ + def __init__(self, parent): + super().__init__(parent) + + self.root = parent + self.frame_dict = {} + self.intial_Frames() + + self.bind("<>", self.watcher) + + + def intial_Frames(self): + frame1 = tk.Frame(self, bg="white") + + self.add(frame1, text="Frame 1") + self.add(tk.Label(self), text="-") + self.add(tk.Label(self), text="+") + + self.frame_dict.update({"Frame 1": frame1}) + + def add_frame_button_func(self): + c = self.index("current") + self.insert_frame(c - 1) + + def insert_frame(self, index): + tab_text = f"Frame {index+1}" + frame = tk.Frame(self, bg="white") + + self.insert(index, frame, text=tab_text) + + self.frame_dict.update({tab_text: frame}) + self.select(index) + + def remove_frame(self, index): + self.forget(index) + self.select(index - 1) + + def get_current_frame_tcl_name(self): + current_index = self.index("current") + return self.root.nametowidget(self.tabs()[current_index]) + + def watcher(self, e): + tab_name = self.tab(self.select(), "text") + + if tab_name not in ("-", "+"): + return + + if tab_name == "-": + c = self.index("current") + if self.index("end") > 3: + self.remove_frame(c - 1) + else: + self.select(c - 1) + + elif tab_name == "+": + c = self.index("current") + self.insert_frame(c - 1) + diff --git a/ttkwidgets/editable_treeview.py b/ttkwidgets/editable_treeview.py new file mode 100644 index 00000000..a4e15939 --- /dev/null +++ b/ttkwidgets/editable_treeview.py @@ -0,0 +1,219 @@ +""" +Author: Faraaz Kurawle +License: GNU GPLv3 +Source: This repository +""" + +from tkinter import ttk +import tkinter as tk + +class PopupEntry(tk.Entry): + """ + Provides a temporary tk.Entry widget which can be used to show a temporaty entry widget to retrive data from user. + After retriving data, it returns the value back and gets destroyed. + + Used internaly by EditableTreeview. + + :param parent: parent of the widget, ideally EditableTreeview + :type parent: widget + + :param x: location of x-axis where PoputEntry would be placed + :type x: integer + + :param y: location of y-axis where PoputEntry would be placed + :type x: integer + + :param textvar: Tkinter varaible which would store and return new value. + :type x: Tkinter Varaible + + :param width: width of the Entry. + :type x: integer + + :param height: height of the Entry + :type x: integer + + :param entry_value: current value inside Entry widget. + :type x: string + + :param options: All valid customization option of tk.Entry widget. + """ + + def __init__( + self, + parent, + x, + y, + textvar, + width, + height, + entry_value="", + **options + ): + super().__init__( + parent, + textvariable=textvar, + **options + ) + self.place(x=x + 1, y=y, width=width, height=height) + + self.textvar = textvar + self.textvar.set(entry_value) + self.focus_set() + self.select_range(0, "end") + # move cursor to the end + self.icursor("end") + + self.wait_var = tk.StringVar(master=self) + + self._bind_widget() + self.wait_window() + + def _bind_widget(self): + self.bind("", self.retrive_value) + self.bind("", self.retrive_value) + + def retrive_value(self, e): + value = self.textvar.get() + self.destroy() + self.textvar.set(value) + +class EditableTreeview(ttk.Treeview): + """Customized Treeview with cell editing feature + + :param parent: parent widget + :type parent: widget + + :param coloums: List of column names for the column heading. + :type columns: tuple + + :param bind_key: key which would trigger editting of the cell + :type bind_key: string in format "" OR "<> + + :noneditable_columns: List of Columns of which values wont be editted. In format "#COLUMN_NUMBER" + :type noneditable_columns: tupple + :note noneditable_columns: #0 is reserved for indexing, therefore all data inserted starts from #1. + + :param treeview_options: all valid treeview options""" + + def __init__( + self, + parent, + columns: tuple, + data: dict, + bind_key="", + non_editable_columns=("",), + **treeview_options + ): + super().__init__(parent,columns=columns, **treeview_options) + self.parent = parent + self.column_name = columns + self.data = data + self.bind_key = bind_key + self.non_editable_columns = non_editable_columns + + self.set_primary_key_column_attributes() + self.set_headings() + self.insert_data() + self.set_edit_bind_key() + + def set_primary_key_column_attributes(self): + self.column("#0", width=100, stretch=1) + + def set_headings(self): + for i in self.column_name: + self.heading(column=i, text=i) + + def insert_data(self): + for values in self.data.items(): + self.insert("", tk.END, values=values) + + + def set_edit_bind_key(self): + self.bind("", self.edit) + + def get_absolute_x_cord(self): + rootx = self.winfo_pointerx() + widgetx = self.winfo_rootx() + + x = rootx - widgetx + + return x + + def get_absolute_y_cord(self): + rooty = self.winfo_pointery() + widgety = self.winfo_rooty() + + y = rooty - widgety + return y + + def get_current_column(self): + pointer = self.get_absolute_x_cord() + return self.identify_column(pointer) + + def get_cell_cords(self, row, column): + return self.bbox(row, column=column) + + def get_selected_cell_cords(self): + row = self.focus() + column = self.get_current_column() + return self.get_cell_cords(row=row, column=column) + + def update_row(self, values, current_row, currentindex): + self.delete(current_row) + self.insert("", currentindex, values=values) + + def check_region(self): + result = self.identify_region( + x=(self.winfo_pointerx() - self.winfo_rootx()), + y=(self.winfo_pointery() - self.winfo_rooty()), + ) + if result == "cell": + return True + else: + return False + + def check_non_editable(self): + if self.get_current_column() in self.non_editable_columns: + return False + else: + return True + + def edit(self, e): + if self.check_region() == False: + return + elif self.check_non_editable() == False: + return + + current_row = self.focus() + currentindex = self.index(self.focus()) + current_row_values = list(self.item(self.focus(), "values")) + current_column = int(self.get_current_column().replace("#", "")) - 1 + current_cell_value = current_row_values[current_column] + + entry_cord = self.get_selected_cell_cords() + entry_x = entry_cord[0] - 1 + entry_y = entry_cord[1] + entry_w = entry_cord[2] + entry_h = entry_cord[3] + + entry_var = tk.StringVar() + + PopupEntry( + self, + x=entry_x, + y=entry_y, + width=entry_w, + height=entry_h, + entry_value=current_cell_value, + textvar=entry_var, + relief="flat", + bg="white", + ) + + if entry_var.get() != current_cell_value: + current_row_values[current_column] = entry_var.get() + self.update_row( + values=current_row_values, + current_row=current_row, + currentindex=currentindex, + ) diff --git a/ttkwidgets/numberedtext.py b/ttkwidgets/numberedtext.py new file mode 100644 index 00000000..bfa7f098 --- /dev/null +++ b/ttkwidgets/numberedtext.py @@ -0,0 +1,114 @@ +""" +Author: Faraaz Kurawle +License: GNU GPLv3 +Source: This repository +""" + +from tkinter import ttk +import tkinter as tk + +class NumberedText(tk.Frame): + """ + Text Widget along with line numbers. + + :param master: parent of this widget. + :type: widget + + param options: all valid tk.Text customization options. + """ + def __init__(self, master, **options): + super().__init__(master) + + self.options = options + self.config(bg='red') + style = ttk.Style(self) + self.configure(bg="white") + style.configure("TSeparator", relief="flat") + + self.uniscrollbar = tk.Scrollbar(self, relief="flat") + self.uniscrollbar.pack(side="right", fill="y") + + self.scroll_text() + + self.number_widget() + + self.textarea.config(spacing1=0, spacing2=0, spacing3=1) + + def scroll_text(self): + self.textarea = tk.Text(self, relief="flat", font="times 15", **self.options) + + self.uniscrollbar["command"] = self.scroll_both + self.textarea["yscrollcommand"] = self.update_scroll_both + + self.textarea.pack(side="right", fill="y") + + def number_widget(self): + self.linenumber = LineNumbers(self, self.textarea, relief="flat", state="disabled") + + self.uniscrollbar["command"] = self.scroll_both + self.linenumber["yscrollcommand"] = self.update_scroll_both + + self.linenumber.pack(side="right", fill="y") + + def mouse_wheel(self, event): + self.scrolltext.yview_scroll(int(-1*(event.delta/120)), "units") + self.number_widget.yview_scroll(int(-1*(event.delta/120)), "units") + + def scroll_both(self, action, position): + self.textarea.yview_moveto(position) + self.linenumber.yview_moveto(position) + + def update_scroll_both(self, first, last, type=None): + self.textarea.yview_moveto(first) + self.linenumber.yview_moveto(first) + self.uniscrollbar.set(first, last) + +class LineNumbers(tk.Listbox): + ### Internal Part of Numbered Text + def __init__(self, master, textwidget, **options): + super().__init__(master, **options) + + self.textwidget = textwidget + self.textwidget.bind("", self.update_num_list) + self.textwidget.bind("", self.update_num_list) + self.textwidget.bind("", self.update_num_list) + + + self.number_var = tk.Variable(self, value=["1"]) + + self.configure(listvariable=self.number_var, selectmode=tk.SINGLE) + self.set_width(1) + self.set_font() + + def set_font(self): + font = self.textwidget.cget("font") + self.configure(font = font) + + def set_width(self, num_len): + self.configure(width=num_len+1) + + def update_num_list(self, event): + linenums = self.get_num_lines() + current_column = self.get_current_colomn() + + if current_column != 0 and event.keycode == 8: return + + if current_column == 0 and linenums == 2 and event.keycode == 8 : return + + number_list = list(range(1, linenums+1)) if event.keycode == 13 or event.keycode == 86 else list(range(1, linenums-1)) + + self.set_width(len(str(linenums))) + self.number_var.set(number_list) + self.yview("end") + + def get_num_lines(self): + num_lines = int(self.textwidget.index("end").split(".")[0]) + return (num_lines) + + def get_current_colomn(self): + curr_column = int(self.textwidget.index("insert").split(".")[1]) + return (curr_column) + + def get_current_row(self): + curr_row = int(self.textwidget.index("insert").split(".")[0]) + return (curr_row)