Source code for ColorPicker

"""Input widget to select a color"""
# Author(s): Davide.De-Marchi@ec.europa.eu
# Copyright © European Union 2022-2023
# 
# Licensed under the EUPL, Version 1.2 or as soon they will be approved by 
# the European Commission subsequent versions of the EUPL (the "Licence");
# 
# You may not use this work except in compliance with the Licence.
# 
# You may obtain a copy of the Licence at:
# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12

# Unless required by applicable law or agreed to in writing, software
# distributed under the Licence is distributed on an "AS IS"
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied.
# 
# See the Licence for the specific language governing permissions and
# limitations under the Licence.
import ipyvuetify as v
from ipywidgets import widgets
from vois import colors
from vois.vuetify import popup
from vois.vuetify.utils.util import *
from typing import Callable, Any, Optional
import warnings


# Popup widget that, on hover, opens a card with complementary, triadic, analogous, etc. clickable colors
class colorTheoryPopup():

    # Initialisation
    def __init__(self, picker):
        
        self.picker = picker
        self._color = self.picker.color

        card_width  = 564
        card_height = 320

        self.card  = v.Card(flat=True, width=card_width, height=card_height, class_='pa-2 ma-0')
        self.popup = popup.popup(self.card, '', text=False, outlined=False, icon='mdi-palette', color='#333333', rounded=False,
                                 buttonwidth=30, buttonheight=self.picker.height+1, popupwidth=card_width, popupheight=card_height)
        self.updateCard()
        
    
    # Update the content of the card to be dispalyed when hover on the popup
    def updateCard(self):
        l_title = self.label('Color theory helpers:', width=300, weight=550)
        
        l_compl = self.label('Complementary colors:', width=160)
        c_col   = self.colorcard(self._color)
        c_compl = self.colorcard(colors.rgb2hex(colors.complementaryColor(colors.string2rgb(self._color))))

        l_tri = self.label('Triadic colors:', width=160)
        tri = [colors.rgb2hex(x) for x in colors.triadicColor(colors.string2rgb(self._color))]
        c_tri1 = self.colorcard(tri[0])
        c_tri2 = self.colorcard(tri[1])

        l_split = self.label('Split complementary col.:', width=160)
        split = [colors.rgb2hex(x) for x in colors.splitComplementaryColor(colors.string2rgb(self._color))]
        c_split1 = self.colorcard(split[0])
        c_split2 = self.colorcard(split[1])

        l_tet = self.label('Tetradic colors:', width=160)
        tet = [colors.rgb2hex(x) for x in colors.tetradicColor(colors.string2rgb(self._color))]
        c_tet1 = self.colorcard(tet[0])
        c_tet2 = self.colorcard(tet[1])
        c_tet3 = self.colorcard(tet[2])
        c_tet4 = self.colorcard(tet[3])

        l_squ = self.label('Square colors:', width=160)
        squ = [colors.rgb2hex(x) for x in colors.squareColor(colors.string2rgb(self._color))]
        c_squ1 = self.colorcard(squ[0])
        c_squ2 = self.colorcard(squ[1])
        c_squ3 = self.colorcard(squ[2])
        
        l_ana = self.label('Analogous colors:', width=160)
        ana = [colors.rgb2hex(x) for x in colors.analogousColor(colors.string2rgb(self._color))]
        c_ana1 = self.colorcard(ana[0])
        c_ana2 = self.colorcard(ana[1])

        l_mono = self.label('Monochromatic colors:', width=160)
        c_mono1 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment=-0.6)))
        c_mono2 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment=-0.4)))
        c_mono3 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment=-0.2)))
        c_mono4 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment= 0.2)))
        c_mono5 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment= 0.4)))
        c_mono6 = self.colorcard(colors.rgb2hex(colors.monochromaticColor(colors.string2rgb(self._color), increment= 0.6)))
        
        w1 = widgets.VBox([
                l_title,
                widgets.HBox([l_compl, c_col, c_compl]),
                widgets.HBox([l_tri,   c_col, c_tri1, c_tri2]),
                widgets.HBox([l_split, c_col, c_split1, c_split2]),
        ])
            
        self.card.children = [
            widgets.VBox([
                widgets.HBox([w1, v.Html(tag='div', style_='width: 84px;'), v.Img(src=colors.colorWheel, width=130, height=130)]),
                widgets.HBox([l_tet,   c_col, c_tet1, c_tet2, c_tet3, c_tet4]),
                widgets.HBox([l_squ,   c_col, c_squ1, c_squ2, c_squ3]),
                widgets.HBox([l_ana,   c_col, c_ana1, c_ana2]),
                widgets.HBox([l_mono,  c_mono1, c_mono2, c_mono3, c_col, c_mono4, c_mono5, c_mono6])
            ])
        ]
        

    # Returns the vuetify object to display
    def draw(self):
        return self.popup.draw()
        
        
    # color property
    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, c):
        if isinstance(c, str):
            self._color = c
            self.updateCard()
        
        
    # disabled property
    @property
    def disabled(self):
        return self.popup.disabled
    
    @disabled.setter
    def disabled(self, flag):
        self.popup.disabled = flag
        
        
    # Creation of a label
    def label(self, text, class_='pa-0 ma-0 mr-3 mb-4', size=14, weight=400, color='#000000', width=None):
        lab = v.Html(tag='div', children=[text], class_=class_)
        if width is None: lab.style_ = 'font-size: %dpx; font-weight: %d; color: %s; overflow: hidden;'%(size,weight,color)
        else:             lab.style_ = 'font-size: %dpx; font-weight: %d; color: %s;  overflow: hidden; width: %dpx'%(size,weight,color,int(width))
        return lab

    
    # Create and returns a card displaying a color and clickable
    def colorcard(self, color, width=54, height=30):

        def onclick(*args):
            self.picker.color = color

        c = v.Card(flat=True, hover=True, color=color, width=width, min_width=width, max_width=width, height=height, tile=True)
        c.on_event('click',onclick)
        return c


    
[docs] class ColorPicker(v.Menu): """ Input widget to select a color. Parameters ---------- color : str, optional Initial color selected on the widget expressed in hexadecimal format '#RRGGBB' (default is '#FF0000') dark : bool, optional If True, the popup color selection will have a dark background (default is settings.dark_mode) dark_text : bool, optional If True, the text on the colored button is displayed in white, if False in black. If dark_text is None, the color of the text is automatically selected based on the currently selected color (default is None) width : int, optional Width of the widget in pixels (default is 40) height : int, optional Height of the widget in pixels (default is 30) rounded : bool, optional If True the color widget is displayed as a round button (default is False) canvas_height : int, optional Height of the canvas displayed on top of the popup window to select the colors (default is True) show_canvas : bool, optional If True the popup window will show the color canvas (default is True) show_mode_switch : bool, optional If True the popup window will show mode switch control among RGB, HSL and HAX (default is True) show_inputs : bool, optional If True the popup window will show the input field for the color components (default is True) show_swatches : bool, optional If True the popup window will show the color swatches (default is True) swatches_max_height : int, optional Height in pixels of the swatches area in the popup window (default is 164) text : str, optional Text to display in the color button (default is '') text_weight : int, optional Weight of the text to be shown in the button (default is 400, Bold is any value greater or equal to 500) on_change : function, optional Python function to call when the user selects a different color. The function will receive the argument parameter. If the argument is None, the function will receive no parameters. (default is None) argument : any, optional Argument to be passed to the onchange function (default is None) offset_x : bool, optional If True the popup window will be opened on the right of the color button (default is False) offset_y : bool, optional If True the popup window will be opened on the bottom of the color button (default is True) disabled : bool, optional True if the selection of the color is disabled, False otherwise (default is False) color_theory_popup : bool, optional If True a popup window (self.ctpopup) is created to show the complementary, analogous, etc.. colors on hover (default is False) Example ------- Creation of a color picker widget to select a color:: from vois.vuetify import ColorPicker from ipywidgets import widgets from IPython.display import display output = widgets.Output() def on_change(): with output: print('Changed to', c.color) c = ColorPicker(color='#00AAFF', width=30, height=30, rounded=False, on_change=on_change, offset_x=True, offset_y=False) display(c) display(output) .. figure:: figures/colorPicker.png :scale: 100 % :alt: colorPicker widget Example of a colorPicker to select a color """ def hex2rgb(self, color): if color[0] == '#': color = color[1:] rgb = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)) return rgb def textDark(self, color): r, g, b = self.hex2rgb(color) if r + g + b <= 255 + 128: return True return False deprecation_alias = dict(textweight='text_weight', onchange='on_change') # Initialization @deprecated_init_alias(**deprecation_alias) def __init__(self, color: str = "#FF0000", dark: bool = None, dark_text: bool = None, width: int = 40, height: int = 30, rounded: bool = False, canvas_height: int = 100, show_canvas: bool = True, show_mode_switch: bool = True, show_inputs: bool = True, show_swatches: bool = True, swatches_max_height: int = 164, text: str = '', text_weight: int = 400, on_change: Optional[Callable[[], None]] = None, argument: Optional[Any] = None, offset_x: bool = False, offset_y: bool = True, disabled: bool = False, color_theory_popup: bool = False): from vois.vuetify import settings self._color = str(color).upper() self.dark = dark if dark is not None else settings.dark_mode self._dark_text = dark_text self.width = width self.height = height self.rounded = rounded self.canvas_height = canvas_height self.show_canvas = show_canvas self.show_mode_switch = show_mode_switch self.show_inputs = show_inputs self.show_swatches = show_swatches self.swatches_max_height = swatches_max_height self.text = text self.text_weight = text_weight self.on_change = on_change self.argument = argument self.offset_x = offset_x self.offset_y = offset_y self._disabled = disabled self.ctpopup = None if color_theory_popup: self.ctpopup = colorTheoryPopup(self) if self._disabled: von = '' else: von = 'menuData.on' if self._dark_text is None: darktext = self.textDark(self._color) else: darktext = self._dark_text self.button = v.Btn(v_on=von, depressed=True, large=False, dense=True, class_='pa-0 ma-0', color=self._color, rounded=self.rounded, height=self.height, width=self.width, min_width=self.width, dark=darktext, style_='font-weight: %d; text-transform: none' % self.text_weight, children=[self.text]) self.p = v.ColorPicker(value=self._color, flat=True, class_="pa-0 ma-0", style_="min-width: 300px;", canvas_height=self.canvas_height, show_swatches=self.show_swatches, dark=self.dark, swatches_max_height=self.swatches_max_height, hide_canvas=not self.show_canvas, hide_mode_switch=not self.show_mode_switch, hide_inputs=not self.show_inputs) self.p.on_event('input', self.__internal_onchange) super().__init__(offset_x=self.offset_x, offset_y=self.offset_y, open_on_hover=False, dense=True, close_on_click=True, close_on_content_click=False, v_slots=[{'name': 'activator', 'variable': 'menuData', 'children': self.button}], children=[self.p]) self.children = [self.p] for alias, new in self.deprecation_alias.items(): create_deprecated_alias(self, alias, new) # Manage 'input' event def __internal_onchange(self, widget, event, data): if data.upper() != self._color.upper(): self._color = data.upper() self.button.color = self._color if self.ctpopup is not None: self.ctpopup.color = self._color if self._dark_text is None: self.button.dark = self.textDark(self._color) if self.on_change: if self.argument is None: self.on_change() else: self.on_change(self.argument) # Returns the vuetify object to display (the v.Menu) def draw(self): warnings.warn('The "draw" method is deprecated, please just use the object widget itself.', category=DeprecationWarning, stacklevel=2) return self # color property @property def color(self): """ Get/Set the selected color. Returns -------- c : str color currently selected Example ------- Programmatically change the color:: picker.color = '#00FF00' print(picker.color) """ return self._color @color.setter def color(self, c): if isinstance(c, str): self._color = c self.p.value = self._color self.button.color = self._color if self.ctpopup is not None: self.ctpopup.color = self._color if self._dark_text is None: self.button.dark = self.textDark(self._color) if self.on_change: if self.argument is None: self.on_change() else: self.on_change(self.argument) # disabled property @property def disabled(self): """ Get/Set the disabled state of the widget. Returns -------- flag : bool True if the widget is disabled, False otherwise Example ------- Programmatically change the disabled state:: picker.disabled = True print(picker.disabled) """ return self._disabled @disabled.setter def disabled(self, flag): self._disabled = bool(flag) if self._disabled: von = '' self.button.children = ['⊗'] # Visual change to let the user know that the widget is disabled else: von = 'menuData.on' self.button.children = [self.text] self.button.v_on = von if self.ctpopup is not None: self.ctpopup.disabled = self._disabled # dark_text property @property def dark_text(self): """ Get/Set the dark flag for the button that displayes the selected color. If True, the text on the colored button is displayed in white, if False in black. If dark_text is None, the color of the text is automatically selected based on the currently selected color Returns -------- flag : bool If True, the text on the colored button is displayed in white, if False in black. If dark_text is None, the color of the text is automatically selected based on the currently selected color Example ------- Programmatically change the dark_text property:: picker.dark_text = True print(picker.dark_text) """ return self._dark_text @dark_text.setter def dark_text(self, flag): self._dark_text = bool(flag) if self._dark_text is None: darktext = self.textDark(self._color) else: darktext = self._dark_text self.button.dark = darktext