Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ideas for new widgets #77

Open
israel-dryer opened this issue Nov 25, 2021 · 21 comments
Open

Ideas for new widgets #77

israel-dryer opened this issue Nov 25, 2021 · 21 comments
Assignees
Labels
enhancement New feature or request

Comments

@israel-dryer
Copy link
Owner

An issue for collecting ideas on new widget styles to add into ttk bootstrap.

@israel-dryer
Copy link
Owner Author

A "progress-scale" widget. Essentially a scale with a trailing trough color that indicates progress up to the slider handle.

Screenshot_20211125-101446.png

@israel-dryer
Copy link
Owner Author

A tooltip widget that can be attached to any other widget

https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/tooltip.htm#BABBIJBJ

@fredcardoso
Copy link

Hi! I don't know if it helps with anything, but I've been using this piece of code from stackoverflow to display tooltips:

class ToolTip(object):
    def __init__(self, widget, text='widget info'):
        self.waittime = 500
        self.wraplength = 280
        self.widget = widget
        self.text = text
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Leave>", self.leave)
        self.widget.bind("<ButtonPress>", self.leave)
        self.id = None
        self.tw = None

    def enter(self, event=None):
        self.schedule()

    def leave(self, event=None):
        self.unschedule()
        self.hidetip()

    def schedule(self):
        self.unschedule()
        self.id = self.widget.after(self.waittime, self.showtip)

    def unschedule(self):
        sid = self.id
        self.id = None
        if sid:
            self.widget.after_cancel(sid)

    def showtip(self, event=None):
        x, y, cx, cy = self.widget.bbox("insert")
        x += self.widget.winfo_rootx() + 25
        y += self.widget.winfo_rooty() + 20
        # creates a toplevel window
        self.tw = Toplevel(self.widget)
        # Leaves only the label and removes the app window
        self.tw.wm_overrideredirect(True)
        self.tw.wm_geometry("+%d+%d" % (x, y))
        label = ttk.Label(self.tw, text=self.text, justify='left',
                          background="#fffddd", relief='flat', borderwidth=10,
                          wraplength=self.wraplength, font=f'{fonte[0]} {fonte[1] - 1}')
        label.pack(ipadx=1)

    def hidetip(self):
        tw = self.tw
        self.tw = None
        if tw:
            tw.destroy()

@israel-dryer israel-dryer self-assigned this Dec 20, 2021
@israel-dryer
Copy link
Owner Author

@fredcardoso, I've created a branch to implement this feature. You can see the class here
tooltip

@daniilS
Copy link
Contributor

daniilS commented Dec 20, 2021

@israel-dryer, I'm working on a scrollable Frame widget with some improvements over most I've seen on github (option to automatically hide scrollbars, cross-platform, mousewheel works even when hovering over contained widgets, less jittery scrolling by limiting frame rate) which I'll be happy to contribute to ttkbootstrap when it's finished. I also hope to port the InteractiveNotebook to 1.0 soon after I've migrated my project from 0.5 .

@israel-dryer
Copy link
Owner Author

@daniilS, that would be great.

@israel-dryer
Copy link
Owner Author

@daniilS, here's an imitation Windows 10 toast notification that would work cross-platform. You can set a delay which will result in a fade-out after delay=x milliseconds. Or, you can click to close. I'm also going to add the option of having 'action' buttons. I'm using the Microsoft toast as the basis of the design.

The components are the icon, title, and text. The icon is just an emoji, which is accessible with the icons.Emoji class (I'm going to make an explorer app for that).

The only issue I might have is that the taskbar covers the (-0, -0) coordinates, so I have to add some padding to the bottom so it doesn't get covered by the taskbar. Not sure how to get around that piece.

toast-short

@israel-dryer israel-dryer linked a pull request Dec 21, 2021 that will close this issue
@israel-dryer israel-dryer removed a link to a pull request Dec 21, 2021
@israel-dryer
Copy link
Owner Author

@daniilS, I've been thinking about this scrolledtext & scrolledframe for a while as well. I've finally worked out a solution that I like with the autohide scrollbar. I decided to experiment with using place instead of pack for the scrollbar, and it's a much smoother effect as it doesn't' cause the window to resize. I've added optional horizontal scrollbars as well.

You can see the scrolled widgets here in a dedicated branch.

Finally, I added a ScrolledFrame widget. This is one I have not been able to find a perfect solution for, but I've decided to go against the grain on what is typically implemented here. I'm exposing the internal frame in the object instead of the container. This has the benefit of being able to be used like a normal frame, instead of having to use a reference to an internal container. There are just a few situations where a reference to the container is needed (Notebook, Panedwindow), but otherwise, it can be used as normal.

HFroWWc1P5

Here is an example of ScrolledText with horizonal and vertical bars.

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledText

app = ttk.Window()

st = ScrolledText(app, padding=20, width=30, height=10, hbar=True)
st.pack(fill=BOTH, expand=YES)

for x in range(25):
    st.insert(END, f'This is a really long line of text... it is {x+1} of 25!\n')

app.mainloop()

image

Here is an example of ScrolledFrame

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledFrame

app = ttk.Window()

sf = ScrolledFrame(app)
sf.pack(fill=BOTH, expand=YES, padx=10, pady=10)

for x in range(20):
    ttk.Button(sf, text=f"button {x}").pack(anchor=W)

app.mainloop()

image

Here is an example of ScrolledFrame in a notebook. You can see here that I have to reference the container for the Notebook.add because the notebook has to use the top of the path.

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledFrame

app = ttk.Window()

nb = ttk.Notebook()
nb.pack(padx=10, pady=10)

sf = ScrolledFrame(app)
nb.add(sf.container, text="ScrolledFrame", padding=10)

for x in range(20):
    ttk.Button(sf, text=f"button {x}").pack(anchor=W)

app.mainloop()

image

@antrrax
Copy link
Contributor

antrrax commented Dec 28, 2021

A suggestion for a widget would be a treeview like KivyMD's datatables
https://kivymd.readthedocs.io/en/latest/components/datatables/

a 'Rows per page' combobox, pagination buttons (first, previous, next, last) and autoscroll (Scrolled treeview) vertical and horizontal and a button to refresh the data

@daniilS
Copy link
Contributor

daniilS commented Dec 28, 2021

@israel-dryer I should have time to take a look at the scrolled frame later this week. I've got a good way to add scrolling to all future child widgets (differing somewhat from the scrollutil implementation, I'll explain the differences later), and have a solution for the scrolling becoming jittery when moving the scrollbar quickly, so can add that functionality straight away. I'm exploring how feasible it would be to add smooth scrolling with the mousewheel.

Re: the suggestion by @antrrax, the tablelist widget looks similar to what you're interested in.

@israel-dryer
Copy link
Owner Author

@antrrax, @daniilS here's a very rough prototype. The example below is paginated with a million records. The pagination can be turned off however.

  • The columns are sortable and there is a sort indicator on the sorted column.
  • I added some buttons on the bottom to update the pages (I'll clean this up).
  • When the table is not paginated the page button frame will not be created and the table will look just like a normal scrolled treeview widget)

I set this up so that the pages are only created in the table when the page is requested. I presume this would improve performance as I do not have to wait for hundreds of thousands of records to get inserted into the table before showing it on the screen.

⭐ The only other feature I can think of that I would like to implement is an optional search bar that would filter the contents of the table.

❓Can you think of other features that would be helpful?

j8jdDMJnvn

@antrrax
Copy link
Contributor

antrrax commented Dec 29, 2021

@israel-dryer

Very useful column sorting option. It would be nice if you could sort columns with the following types of information
string, integers, floats, date and time, codes, currency, percentage

Some Text, 5, 12.1, 2021-08-31, 2.5.2.1, U$5.30, 15%

codes may have several and mixed separators (. - /) = (8980.046.688.998.890) (156.799.179-39) (00776574/0001-56)
However, there are some regional peculiarities of each country.
Standard Currency Type - U$, £$, R$ ....
Date format - there are several date formations (datetime.datetime.strptime(string, format)) where format='%Y-%m-%d' or format='%d/%m/%Y' ...
Decimal Separator - Floatad can use semicolons (12.1 or 12,1)

see an example of sort (incomplete)
https://stackoverflow.com/questions/69778143/python-treeview-alternate-row-colouring-after-sorting


Another useful option would be to color the lines in the treeview. And differentiate the even lines from the odd ones creating a zebra effect.
main background color
Color when selecting line
Even line color (primary)
Odd thread color (secondary)


A button to update table data and
Instead of a combobox to display the page as I suggested earlier.
It would be more interesting an entry to type the desired page as in the image:
01


Another more advanced option would be a submenu in the header to show or hide the columns in the treeview. (I don't know if this would be useful for all people in all cases)

Here is an image of a Crud with this feature.

Clicking on the header sorts the data.

With the mouse over in the header, it displays a sub-menu option with the following data:
02
Sort in ascending order
Sort in descending order
Columns > another subemnu with the treeview column names [check item]

@israel-dryer
Copy link
Owner Author

The data is sorted independent of the treeview values (which I think is always converted to a string), so as long as the values in the row data are in the data type you specify, it will sort by that value type.

I've updated the pagination frame and added search bar.

R6774sUTQ8
.

@daniilS
Copy link
Contributor

daniilS commented Dec 30, 2021

@israel-dryer haven't gotten to the scrolled frame yet, but took a look at what you asked about the toast notification. On Windows, here's a good way to display the toast in the corner of the screen. It accounts for the taskbar location/size, multiple monitors (which may have different DPIs), and works at any DPI awareness context. I'm not sure if equivalent API calls exist for Linux and macOS, but if not then maybe creating an invisible Toplevel in the zoomed state with and without overrideredirect set, and querying wm_geometry and winfo_geometry could help.

import ctypes
import tkinter as tk

user32 = ctypes.windll.user32

# See the links in https://stackoverflow.com/a/4631379/6660529
SPI_GETWORKAREA = 0x30
SM_CXSCREEN = 0
SM_CYSCREEN = 1


# Adapted from
# https://github.com/saveenr/saveenr/blob/master/Demos/python/ui_get_desktop_workarea.py
class RECT(ctypes.Structure):
    _fields_ = [
        ("x0", ctypes.c_ulong),
        ("y0", ctypes.c_ulong),
        ("x1", ctypes.c_ulong),
        ("y1", ctypes.c_ulong),
    ]


# Gets the usable area (excluding things like the taskbar) of the primary monitor. See
# the link at the top for API calls for other monitors.
workarea = RECT()
user32.SystemParametersInfoA(SPI_GETWORKAREA, 0, ctypes.byref(workarea), 0)

# Not used in this example, but useful in other cases, e.g. if you want to display a
# window in the middle of the screen.
primary_monitor_width = user32.GetSystemMetrics(SM_CXSCREEN)
primary_monitor_height = user32.GetSystemMetrics(SM_CYSCREEN)

popup_width = 200
popup_height = 100
# Distance from the corner of the work area
margin = 10

# Position of the top left corner of the window
position_x = workarea.x1 - popup_width - margin
position_y = workarea.y1 - popup_height - margin

popup = tk.Tk()
popup.overrideredirect(True)
popup.geometry(f"{popup_width}x{popup_height}+{position_x}+{position_y}")
popup.mainloop()

@antrrax
Copy link
Contributor

antrrax commented Jan 4, 2022

A suggestion of 2 widgets

Entry with placeholder and menu:
Undo, redo, cut, copy, paste, delete, select all

The other entry widget that inherits the above options and adds a hide and show password button. It also shows a hint when the password is hidden and something is typed with caps lock enabled.
E1E2E4

The menu and shortcuts are simple functions, but they are very much needed in tkinter

Here I use this code, it's far from being professional, the undo and redo part is confusing but it works.
edited, fixed bug:
https://www.mediafire.com/file/e9w121c9cmhynic/custom_entry_entry_pwd.py.zip/file

Feel free to change the code, redo it from scratch or get inspired by it.

You would also need to add these styles to the theme layout.

        self.style.configure('pwdimg.TButton', padding=[5,0])
        self.style.configure('capslock.danger.Inverse.TLabel', font=('', 12, 'bold'))
        self.style.configure('ph.TEntry', foreground='#a3a3a3')

@israel-dryer israel-dryer added the enhancement New feature or request label Jan 9, 2022
@antrrax
Copy link
Contributor

antrrax commented Jan 9, 2022

A Widgets suggestion for Api would be to redo the filedialogs and the color dialog for a cross-platform standardization.

https://pythonbasics.org/tkinter-filedialog/

tkinter.filedialog.asksaveasfilename()
tkinter.filedialog.asksaveasfile()
tkinter.filedialog.askopenfilename()
tkinter.filedialog.askopenfile()
tkinter.filedialog.askdirectory()
tkinter.filedialog.askopenfilenames()
tkinter.filedialog.askopenfiles()

These rederized dialogs in Linux have a very ugly and outdated look. See screenshot, it uses menubuttom instead of combobox and with ttk.bootstrap it visually gets even weirder with these components in the primary com of the theme.

When you have free time, it would be interesting to create components to include in the Api. Get inspired by Windows dialogs or Qt Dialogs that have a beautiful visual appearance.

ttk-filedialog

ttk-filedialog


color dialog windows:
color

color dialog Linux:
color

@lolghuiy4tgfyu4th7tvtg
Copy link

This could be too complex because of tkinter's limitations, but could there be a VideoPlayer widget similar to the one in KivyMD?

@israel-dryer
Copy link
Owner Author

This could be too complex because of tkinter's limitations, but could there be a VideoPlayer widget similar to the one in KivyMD?

The video component is not difficult. But it's the sound that is problematic.

@israel-dryer israel-dryer pinned this issue Feb 7, 2022
@lolghuiy4tgfyu4th7tvtg
Copy link

I do know of this one tutorial of how to make a tkinter music player, which includes a couple parts about scrubbing between song positions, but this only works with MP3 and OGG files so the video would have to be split with ffmpeg or a similar utility.

(the tutorial by the way): https://www.youtube.com/playlist?list=PL2P1yZHTR7QtY2flZuHWjz8Z46W4QJRsO

@antrrax
Copy link
Contributor

antrrax commented Jul 3, 2022

A suggestion would be to add a vertical and horizontal ScrolledFrame like this one:
https://gist.github.com/novel-yet-trivial/2841b7b640bba48928200ff979204115

DoubleScrolledFrame:

try:
    import tkinter as tk
    from tkinter import ttk
except ImportError:
    import Tkinter as tk
    import ttk

class DoubleScrolledFrame:
    """
    A vertically scrolled Frame that can be treated like any other Frame
    ie it needs a master and layout and it can be a master.
    keyword arguments are passed to the underlying Frame
    except the keyword arguments 'width' and 'height', which
    are passed to the underlying Canvas
    note that a widget layed out in this frame will have Canvas as self.master,
    if you subclass this there is no built in way for the children to access it.
    You need to provide the controller separately.
    """
    def __init__(self, master, **kwargs):
        width = kwargs.pop('width', None)
        height = kwargs.pop('height', None)
        self.outer = tk.Frame(master, **kwargs)

        self.vsb = ttk.Scrollbar(self.outer, orient=tk.VERTICAL)
        self.vsb.grid(row=0, column=1, sticky='ns')
        self.hsb = ttk.Scrollbar(self.outer, orient=tk.HORIZONTAL)
        self.hsb.grid(row=1, column=0, sticky='ew')
        self.canvas = tk.Canvas(self.outer, highlightthickness=0, width=width, height=height)
        self.canvas.grid(row=0, column=0, sticky='nsew')
        self.outer.rowconfigure(0, weight=1)
        self.outer.columnconfigure(0, weight=1)
        self.canvas['yscrollcommand'] = self.vsb.set
        self.canvas['xscrollcommand'] = self.hsb.set
        # mouse scroll does not seem to work with just "bind"; You have
        # to use "bind_all". Therefore to use multiple windows you have
        # to bind_all in the current widget
        self.canvas.bind("<Enter>", self._bind_mouse)
        self.canvas.bind("<Leave>", self._unbind_mouse)
        self.vsb['command'] = self.canvas.yview
        self.hsb['command'] = self.canvas.xview

        self.inner = tk.Frame(self.canvas)
        # pack the inner Frame into the Canvas with the topleft corner 4 pixels offset
        self.canvas.create_window(4, 4, window=self.inner, anchor='nw')
        self.inner.bind("<Configure>", self._on_frame_configure)

        self.outer_attr = set(dir(tk.Widget))

    def __getattr__(self, item):
        if item in self.outer_attr:
            # geometry attributes etc (eg pack, destroy, tkraise) are passed on to self.outer
            return getattr(self.outer, item)
        else:
            # all other attributes (_w, children, etc) are passed to self.inner
            return getattr(self.inner, item)

    def _on_frame_configure(self, event=None):
        x1, y1, x2, y2 = self.canvas.bbox("all")
        height = self.canvas.winfo_height()
        width = self.canvas.winfo_width()
        self.canvas.config(scrollregion = (0,0, max(x2, width), max(y2, height)))

    def _bind_mouse(self, event=None):
        self.canvas.bind_all("<4>", self._on_mousewheel)
        self.canvas.bind_all("<5>", self._on_mousewheel)
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

    def _unbind_mouse(self, event=None):
        self.canvas.unbind_all("<4>")
        self.canvas.unbind_all("<5>")
        self.canvas.unbind_all("<MouseWheel>")
        
    def _on_mousewheel(self, event):
        """Linux uses event.num; Windows / Mac uses event.delta"""
        func = self.canvas.xview_scroll if event.state & 1 else self.canvas.yview_scroll 
        if event.num == 4 or event.delta > 0:
            func(-1, "units" )
        elif event.num == 5 or event.delta < 0:
            func(1, "units" )
    
    def __str__(self):
        return str(self.outer)

#  **** SCROLL BAR TEST *****
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if __name__ == "__main__":
    root = tk.Tk()
    root.title("Scrollbar Test")
    root.geometry('400x500')
    lbl = tk.Label(root, text="Hold shift while using the scroll wheel to scroll horizontally")
    lbl.pack()
    
    # use the Scrolled Frame just like any other Frame
    frame = DoubleScrolledFrame(root, width=300, borderwidth=2, relief=tk.SUNKEN, background="light gray")
    #frame.grid(column=0, row=0, sticky='nsew') # fixed size
    frame.pack(fill=tk.BOTH, expand=True) # fill window

    for i in range(30):
        for j in range(20):
            label = tk.Label(frame, text="{}{}".format(alphabet[j], i), relief='ridge')
            label.grid(column=j, row=i, sticky='ew', padx=2, pady=2)

    root.mainloop()

@git9t
Copy link

git9t commented Apr 15, 2024

Hello sir, How to make scrolltext not editable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants