pep-mklive/pylibraries/ttkbootstrap/dialogs/colorchooser.py

571 lines
22 KiB
Python

import ttkbootstrap as ttk
from ttkbootstrap.validation import add_range_validation, add_validation, validator
from ttkbootstrap.constants import *
from tkinter import Frame as tkFrame
from tkinter import Label as tkLabel
from ttkbootstrap import utility
from collections import namedtuple
from ttkbootstrap import colorutils
from ttkbootstrap.colorutils import RGB, HSL, HEX, HUE, SAT, LUM
from PIL import ImageColor
from ttkbootstrap.dialogs.colordropper import ColorDropperDialog
from ttkbootstrap.tooltip import ToolTip
from ttkbootstrap.localization import MessageCatalog
STD_SHADES = [0.9, 0.8, 0.7, 0.4, 0.3]
STD_COLORS = [
'#FF0000', '#FFC000', '#FFFF00', '#00B050',
'#0070C0', '#7030A0', '#FFFFFF', '#000000'
]
ColorValues = namedtuple('ColorValues', 'h s l r g b hex')
ColorChoice = namedtuple('ColorChoice', 'rgb hsl hex')
PEN = ''
@validator
def validate_color(event):
try:
ImageColor.getrgb(event.postchangetext)
return True
except:
return False
class ColorChooser(ttk.Frame):
"""A class which creates a color chooser widget
![](../../assets/dialogs/querybox-get-color.png)
"""
def __init__(self, master, initialcolor=None, padding=None):
super().__init__(master, padding=padding)
self.tframe = ttk.Frame(self, padding=5)
self.tframe.pack(fill=X)
self.bframe = ttk.Frame(self, padding=(5, 0, 5, 5))
self.bframe.pack(fill=X)
self.notebook = ttk.Notebook(self.tframe)
self.notebook.pack(fill=BOTH)
self.style = ttk.Style.get_instance()
self.colors = self.style.colors
self.initialcolor = initialcolor or self.colors.bg
# color variables
r, g, b = ImageColor.getrgb(self.initialcolor)
h, s, l = colorutils.color_to_hsl((r, g, b), RGB)
hx = colorutils.color_to_hex((r, g, b), RGB)
self.hue = ttk.IntVar(value=h)
self.sat = ttk.IntVar(value=s)
self.lum = ttk.IntVar(value=l)
self.red = ttk.IntVar(value=r)
self.grn = ttk.IntVar(value=g)
self.blu = ttk.IntVar(value=b)
self.hex = ttk.StringVar(value=hx)
# widget sizes (adjusted by widget scaling)
self.spectrum_height = utility.scale_size(self, 240)
self.spectrum_width = utility.scale_size(self, 530) # looks better on Mac OS
#self.spectrum_width = utility.scale_size(self, 480)
self.spectrum_point = utility.scale_size(self, 12)
# build widgets
spectrum_frame = ttk.Frame(self.notebook)
self.color_spectrum = self.create_spectrum(spectrum_frame)
self.color_spectrum.pack(fill=X, side=TOP)
self.luminance_scale = self.create_luminance_scale(self.tframe)
self.luminance_scale.pack(fill=X)
self.notebook.add(spectrum_frame, text=MessageCatalog.translate('Advanced'))
themed_colors = [self.colors.get(c) for c in self.style.colors]
self.themed_swatches = self.create_swatches(
self.notebook, themed_colors)
self.standard_swatches = self.create_swatches(
self.notebook, STD_COLORS)
self.notebook.add(self.themed_swatches, text=MessageCatalog.translate('Themed'))
self.notebook.add(self.standard_swatches, text=MessageCatalog.translate('Standard'))
preview_frame = self.create_preview(self.bframe)
preview_frame.pack(side=LEFT, fill=BOTH, expand=YES, padx=(0, 5))
self.color_entries = self.create_value_inputs(self.bframe)
self.color_entries.pack(side=RIGHT)
self.create_spectrum_indicator()
self.create_luminance_indicator()
def create_spectrum(self, master):
"""Create the color spectrum canvas"""
# canvas and point dimensions
width = self.spectrum_width
height = self.spectrum_height
xf = yf = self.spectrum_point
# create canvas widget and binding
canvas = ttk.Canvas(master, width=width, height=height, cursor='tcross')
canvas.bind("<B1-Motion>", self.on_spectrum_interaction, add="+")
canvas.bind("<Button-1>", self.on_spectrum_interaction, add="+")
# add color points
for x, colorx in enumerate(range(0, width, xf)):
for y, colory in enumerate(range(0, height, yf)):
values = self.color_from_coords(colorx, colory)
fill = values.hex
bbox = [x*xf, y*yf, (x*xf)+xf, (y*yf)+yf]
canvas.create_rectangle(*bbox, fill=fill, width=0)
return canvas
def create_spectrum_indicator(self):
"""Create a square indicator that displays in the position of
the selected color"""
s = utility.scale_size(self, 10)
width = utility.scale_size(self, 2)
values = self.get_variables()
x1, y1 = self.coords_from_color(values.hex)
colorutils.contrast_color(values.hex, 'hex')
tag = ['spectrum-indicator']
self.color_spectrum.create_rectangle(
x1, y1, x1+s, y1+s, width=width, tags=[tag])
self.color_spectrum.tag_lower('spectrum-indicator')
# widget builder methods
def create_swatches(self, master, colors):
"""Create a grid of color swatches"""
boxpadx = 2
boxpady = 0
padxtotal = (boxpadx*15)
boxwidth = int((self.spectrum_width-padxtotal)) / len(STD_COLORS)
boxheight = int((self.spectrum_height-boxpady) / (len(STD_SHADES)+1))
container = ttk.Frame(master)
# create color combinations
color_rows = [colors]
lastcol = len(colors)-1
for l in STD_SHADES:
lum = int(l*LUM)
row = []
for color in colors:
color = colorutils.update_hsl_value(
color=color,
lum=lum,
inmodel='hex',
outmodel='hex'
)
row.append(color)
color_rows.append(row)
# themed colors - regular colors
for row in color_rows:
rowframe = ttk.Frame(container)
for j, color in enumerate(row):
swatch = tkFrame(
master=rowframe,
bg=color,
width=boxwidth,
height=boxheight,
autostyle=False
)
swatch.bind('<Button-1>', self.on_press_swatch)
if j == 0:
swatch.pack(side=LEFT, padx=(0, boxpadx))
elif j == lastcol:
swatch.pack(side=LEFT, padx=(boxpadx, 0))
else:
swatch.pack(side=LEFT, padx=boxpadx)
rowframe.pack(fill=X, expand=YES)
return container
def create_preview(self, master):
"""Create the preview frame for original and new colors"""
nbstyle = self.notebook.cget('style')
# set the border color to match the notebook border color
bordercolor = self.style.lookup(nbstyle, 'bordercolor')
container = ttk.Frame(master)
# the frame and label for the original color (current)
old = tkFrame(
master=container,
relief=FLAT,
bd=2,
highlightthickness=1,
highlightbackground=bordercolor,
bg=self.initialcolor,
autostyle=False
)
old.pack(side=LEFT, fill=BOTH, expand=YES, padx=(0, 2))
contrastfg = colorutils.contrast_color(
color=self.initialcolor,
model='hex',
)
tkLabel(
master=old,
text=MessageCatalog.translate('Current'),
background=self.initialcolor,
foreground=contrastfg,
autostyle=False,
width=7
).pack(anchor=NW)
# the frame and label for the new color
self.preview = tkFrame(
master=container,
relief=FLAT,
bd=2,
highlightthickness=1,
highlightbackground=bordercolor,
bg=self.initialcolor,
autostyle=False
)
self.preview.pack(side=LEFT, fill=BOTH, expand=YES, padx=(2, 0))
self.preview_lbl = tkLabel(
master=self.preview,
text=MessageCatalog.translate('New'),
background=self.initialcolor,
foreground=contrastfg,
autostyle=False,
width=7
)
self.preview_lbl.pack(anchor=NW)
return container
def create_value_inputs(self, master):
"""Create color value input widgets"""
container = ttk.Frame(master)
for x in range(4):
container.columnconfigure(x, weight=1)
# value labels
lbl_cnf = {'master': container, 'anchor': E}
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Hue')}:''').grid(row=0, column=0, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Sat')}:''').grid(row=1, column=0, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Lum')}:''').grid(row=2, column=0, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Hex')}:''').grid(row=3, column=0, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Red')}:''').grid(row=0, column=2, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Green')}:''').grid(row=1, column=2, sticky=E)
ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Blue')}:''').grid(row=2, column=2, sticky=E)
# value spinners and entry widgets
rgb_cnf = {'master': container, 'from_': 0, 'to': 255, 'width': 3}
sl_cnf = {'master': container, 'from_': 0, 'to': 100, 'width': 3}
hue_cnf = {'master': container, 'from_': 0, 'to': 360, 'width': 3}
sb_hue = ttk.Spinbox(**hue_cnf, textvariable=self.hue)
sb_hue.grid(row=0, column=1, padx=4, pady=2, sticky=EW)
sb_sat = ttk.Spinbox(**sl_cnf, textvariable=self.sat)
sb_sat.grid(row=1, column=1, padx=4, pady=2, sticky=EW)
sb_lum = ttk.Spinbox(**sl_cnf, textvariable=self.lum)
sb_lum.grid(row=2, column=1, padx=4, pady=2, sticky=EW)
sb_red = ttk.Spinbox(**rgb_cnf, textvariable=self.red)
sb_red.grid(row=0, column=3, padx=4, pady=2, sticky=EW)
sb_grn = ttk.Spinbox(**rgb_cnf, textvariable=self.grn)
sb_grn.grid(row=1, column=3, padx=4, pady=2, sticky=EW)
sb_blu = ttk.Spinbox(**rgb_cnf, textvariable=self.blu)
sb_blu.grid(row=2, column=3, padx=4, pady=2, sticky=EW)
ent_hex = ttk.Entry(container, textvariable=self.hex)
ent_hex.grid(row=3, column=1, padx=4, columnspan=3, pady=2, sticky=EW)
# add input validation
add_validation(ent_hex, validate_color)
add_range_validation(sb_hue, 0, 360)
for sb in [sb_sat, sb_lum]:
add_range_validation(sb, 0, 100)
for sb in [sb_red, sb_grn, sb_blu]:
add_range_validation(sb, 0, 255)
# event binding for updating colors on value change
for sb in [sb_hue, sb_sat, sb_lum]:
for sequence in ['<<Increment>>', '<<Decrement>>', '<Return>', '<KP_Enter>']:
sb.bind(
sequence=sequence,
func=lambda _, w=sb: self.on_entry_value_change(
w, HSL),
add="+"
)
for sb in [sb_red, sb_grn, sb_blu]:
for sequence in ['<<Increment>>', '<<Decrement>>', '<Return>', '<KP_Enter>']:
sb.bind(
sequence=sequence,
func=lambda _, w=sb: self.on_entry_value_change(
w, RGB),
add="+"
)
for sequence in ['<Return>', '<KP_Enter>']:
ent_hex.bind(
sequence=sequence,
func=lambda _, w=ent_hex: self.on_entry_value_change(
w, HEX),
add="+"
)
return container
def create_luminance_scale(self, master):
"""Create the color luminance canvas"""
# widget dimensions
height = xf = self.spectrum_point
width = self.spectrum_width
values = self.get_variables()
canvas = ttk.Canvas(master, height=height, width=width)
# add color points to scale
for x, l in enumerate(range(0, width, xf)):
lum = l/width*LUM
fill = colorutils.update_hsl_value(
color=values.hex,
lum=lum,
inmodel='hex',
outmodel='hex'
)
bbox = [x*xf, 0, (x*xf)+xf, height]
tag = f'color{x}'
canvas.create_rectangle(*bbox, fill=fill, width=0, tags=[tag])
canvas.bind("<B1-Motion>", self.on_luminance_interaction, add="+")
canvas.bind("<Button-1>", self.on_luminance_interaction, add="+")
return canvas
def create_luminance_indicator(self):
"""Create an indicator that displays in the position of the
luminance value"""
lum = 50
x1 = int(lum / LUM * self.spectrum_width) - \
((self.spectrum_point - 2)//2)
y1 = 0
x2 = x1 + self.spectrum_point
y2 = self.spectrum_point - 3
tag = 'luminance-indicator'
bbox = [x1, y1, x2, y2]
self.luminance_scale.create_rectangle(
*bbox, fill='white', outline='black', tags=[tag])
self.luminance_scale.tag_lower(tag)
def coords_from_color(self, hexcolor):
"""Get the coordinates on the color spectrum from the color
value"""
h, s, _ = colorutils.color_to_hsl(hexcolor)
x = (h / HUE) * self.spectrum_width
y = (1-(s / SAT)) * self.spectrum_height
return x, y
def color_from_coords(self, x, y):
"""Get the color value from the mouse position in the color
spectrum"""
HEIGHT = self.spectrum_height
WIDTH = self.spectrum_width
h = int(min(HUE, max(0, (HUE/WIDTH) * x)))
s = int(min(SAT, max(0, SAT - ((SAT/HEIGHT) * y))))
l = 50
hx = colorutils.color_to_hex([h, s, l], 'hsl')
r, g, b = colorutils.color_to_rgb(hx)
return ColorValues(h, s, l, r, g, b, hx)
def set_variables(self, h, s, l, r, g, b, hx):
"""Update the color value variables"""
self.hue.set(h)
self.sat.set(s)
self.lum.set(l)
self.red.set(r)
self.grn.set(g)
self.blu.set(b)
self.hex.set(hx)
def get_variables(self):
"""Get the values of all color models and return a
tuple of color values"""
h = self.hue.get()
s = self.sat.get()
l = self.lum.get()
r = self.red.get()
g = self.grn.get()
b = self.blu.get()
hx = self.hex.get()
return ColorValues(h, s, l, r, g, b, hx)
def update_preview(self):
"""Update the color in the preview frame"""
hx = self.hex.get()
fg = colorutils.contrast_color(
color=hx,
model='hex',
)
self.preview.configure(bg=hx)
self.preview_lbl.configure(bg=hx, fg=fg)
def update_luminance_scale(self):
"""Update the luminance scale with the change in hue and saturation"""
values = self.get_variables()
width = self.spectrum_width
xf = self.spectrum_point
for x, l in enumerate(range(0, width, xf)):
lum = l/width*LUM
fill = colorutils.update_hsl_value(
color=values.hex,
lum=lum,
inmodel='hex',
outmodel='hex'
)
tag = f'color{x}'
self.luminance_scale.itemconfig(tag, fill=fill)
def update_luminance_indicator(self):
"""Update the position of the luminance indicator"""
lum = self.lum.get()
x = int(lum / LUM * self.spectrum_width) - \
((self.spectrum_point - 2)//2)
self.luminance_scale.moveto('luminance-indicator', x, 0)
self.luminance_scale.tag_raise('luminance-indicator')
def update_spectrum_indicator(self):
"""Move the spectrum indicator to a new location"""
values = self.get_variables()
x, y = self.coords_from_color(values.hex)
# move to the new color location
self.color_spectrum.moveto('spectrum-indicator', x, y)
self.color_spectrum.tag_raise('spectrum-indicator')
# adjust the outline color based on contrast of background
color = colorutils.contrast_color(values.hex, 'hex')
self.color_spectrum.itemconfig('spectrum-indicator', outline=color)
# color events
def sync_color_values(self, model):
"""Callback for when a color value changes. A change in one
value will automatically update the other values so that all
color models remain in sync."""
values = self.get_variables()
if model == HEX:
hx = values.hex
r, g, b = colorutils.color_to_rgb(hx)
h, s, l = colorutils.color_to_hsl(hx)
elif model == RGB:
r, g, b = values.r, values.g, values.b
h, s, l = colorutils.color_to_hsl([r, g, b], 'rgb')
hx = colorutils.color_to_hex([r, g, b])
elif model == HSL:
h, s, l = values.h, values.s, values.l
r, g, b = colorutils.color_to_rgb([h, s, l], 'hsl')
hx = colorutils.color_to_hex([h, s, l], 'hsl')
self.set_variables(h, s, l, r, g, b, hx)
self.update_preview()
self.update_luminance_indicator()
def on_entry_value_change(self, widget: ttk.Spinbox, model):
"""Update the widget colors when the color value input is
changed"""
is_valid = widget.validate()
if is_valid:
self.sync_color_values(model)
self.update_luminance_scale()
self.update_spectrum_indicator()
def on_press_swatch(self, event):
"""Update the widget colors when a color swatch is clicked."""
button: tkFrame = self.nametowidget(event.widget)
color = button.cget('background')
self.hex.set(color)
self.sync_color_values(HEX)
self.update_luminance_scale()
self.update_spectrum_indicator()
def on_spectrum_interaction(self, event):
"""Update the widget colors when the color spectrum canvas is
pressed"""
values = self.color_from_coords(event.x, event.y)
self.hue.set(values.h)
self.sat.set(values.s)
self.lum.set(values.l)
self.sync_color_values(HSL)
self.update_luminance_scale()
self.update_spectrum_indicator()
def on_luminance_interaction(self, event):
"""Update the widget colors when the color luminance scale is
pressed"""
l = max(0, min(LUM, int((event.x / self.spectrum_width) * LUM)))
self.lum.set(l)
self.sync_color_values(HSL)
from ttkbootstrap.dialogs import Dialog
class ColorChooserDialog(Dialog):
"""A class which displays a color chooser dialog. When a color
option is selected and the "OK" button is pressed, the dialog will
return a namedtuple that contains the color values for rgb, hsl, and
hex. These values can be accessed by indexing the tuple or by using
the named fields.
![](../../assets/dialogs/querybox-get-color.png)
Examples:
```python
>>> cd = ColorChooserDialog()
>>> cd.show()
>>> colors = cd.result
>>> colors.hex
'#5fb04f'
>>> colors[2]
'#5fb04f
>>> colors.rgb
(95, 176, 79)
>>> colors[0]
(95, 176, 79)
```
"""
def __init__(self, parent=None, title="Color Chooser", initialcolor=None):
title = MessageCatalog.translate(title)
super().__init__(parent=parent, title=title)
self.initialcolor = initialcolor
self.dropper = ColorDropperDialog()
self.dropper.result.trace_add('write', self.trace_dropper_color)
def create_body(self, master):
self.colorchooser = ColorChooser(master, self.initialcolor)
self.colorchooser.pack(fill=BOTH, expand=YES)
def create_buttonbox(self, master):
frame = ttk.Frame(master, padding=(5, 5))
# OK button
ok = ttk.Button(frame, bootstyle=PRIMARY, width=6, text=MessageCatalog.translate('OK'))
ok.bind("<Return>", lambda _: ok.invoke())
ok.configure(command=lambda b=ok: self.on_button_press(b))
ok.pack(padx=2, side=RIGHT)
# Cancel button
cancel = ttk.Button(frame, bootstyle=SECONDARY, width=6, text=MessageCatalog.translate('Cancel'))
cancel.bind("<Return>", lambda _: cancel.invoke())
cancel.configure(command=lambda b=cancel: self.on_button_press(b))
cancel.pack(padx=2, side=RIGHT)
# color dropper (not supported on Mac OS)
if self._toplevel.winsys != 'aqua':
dropper = ttk.Label(frame, text=PEN, font=('-size 16'))
ToolTip(dropper, MessageCatalog.translate('color dropper')) # add tooltip
dropper.pack(side=RIGHT, padx=2)
dropper.bind("<Button-1>", self.on_show_colordropper)
frame.pack(side=BOTTOM, fill=X, anchor=S)
def on_show_colordropper(self, event):
self.dropper.show()
def trace_dropper_color(self, *_):
values = self.dropper.result.get()
self.colorchooser.hex.set(values[2])
self.colorchooser.sync_color_values('hex')
def on_button_press(self, button):
if button.cget('text') == 'OK':
values = self.colorchooser.get_variables()
self._result = ColorChoice(
rgb=(values.r, values.g, values.b),
hsl=(values.h, values.s, values.l),
hex=values.hex
)
self._toplevel.destroy()
self._toplevel.destroy()