diff --git a/kivyblocks/__init__.py b/kivyblocks/__init__.py index e69de29..56bebc6 100644 --- a/kivyblocks/__init__.py +++ b/kivyblocks/__init__.py @@ -0,0 +1,9 @@ +import os +from kivy import logger +__version_info__ = (0.5.0) +__version__ = '0.5.0' + +path = os.path.dirname(__file__) +fonts_path = os.path.join(path,"ttf/") +images_path = os.path.join(path,'imgs/') +logger.info("kivyblocks:kivblocks version:{}".format(__version__) diff --git a/kivyblocks/baseWidget.py b/kivyblocks/baseWidget.py index ebd54cf..ab44238 100755 --- a/kivyblocks/baseWidget.py +++ b/kivyblocks/baseWidget.py @@ -55,6 +55,7 @@ from .widgetExt.inputext import FloatInput,IntegerInput, \ StrInput,SelectInput, BoolInput, AmountInput from .widgetExt.messager import Messager from .charts.bar import Bar +from .tree import Tree, TextTree if platform == 'android': from .widgetExt.phonebutton import PhoneButton @@ -62,7 +63,11 @@ if platform == 'android': class PressableImage(ButtonBehavior,AsyncImage): def on_press(self): - print + pass + +class PressableLabel(ButtonBehavior, Label): + def on_press(self): + pass class Text(Label): bgColor = ListProperty([0.5,0.5,0.5,1]) diff --git a/kivyblocks/blocksapp.py b/kivyblocks/blocksapp.py index bef19ca..ab181bc 100644 --- a/kivyblocks/blocksapp.py +++ b/kivyblocks/blocksapp.py @@ -28,6 +28,7 @@ from .utils import * from .pagescontainer import PageContainer from .widgetExt.messager import Messager from .blocks import Blocks +from .theming import ThemeManager from appPublic.rsa import RSA class ServerInfo: @@ -68,6 +69,7 @@ class BlocksApp(App): def build(self): config = getConfig() self.config = config + self.theme_cls = ThemeManager() self.serverinfo = ServerInfo() self.title = 'Test Title' self.blocks = Blocks() diff --git a/kivyblocks/color_definitions.py b/kivyblocks/color_definitions.py new file mode 100644 index 0000000..24cb622 --- /dev/null +++ b/kivyblocks/color_definitions.py @@ -0,0 +1,676 @@ +colors = { + 'Red': { + '200': 'ef9a9a', + '900': 'b71c1c', + '600': 'e53935', + 'A100': 'ff8a80', + '300': 'e57373', + 'A400': 'ff1744', + '700': 'd32f2f', + '50': 'ffebee', + 'A700': 'd50000', + '400': 'ef5350', + '100': 'ffcdd2', + '800': 'c62828', + 'A200': 'ff5252', + '500': 'f44336' + }, + 'Pink': { + '50': 'fce4ec', + '100': 'f8bbd0', + '200': 'f48fb1', + '300': 'f06292', + '400': 'ec407a', + '500': 'e91e63', + '600': 'd81b60', + '700': 'C2185B', + '800': 'ad1457', + '900': '88e4ff', + 'A100': 'ff80ab', + 'A400': 'F50057', + 'A700': 'c51162', + 'A200': 'ff4081' + }, + 'Purple': { + '200': 'ce93d8', + '900': '4a148c', + '600': '8e24aa', + 'A100': 'ea80fc', + '300': 'ba68c8', + 'A400': 'D500F9', + '700': '7b1fa2', + '50': 'f3e5f5', + 'A700': 'AA00FF', + '400': 'ab47bc', + '100': 'e1bee7', + '800': '6a1b9a', + 'A200': 'e040fb', + '500': '9c27b0' + }, + 'DeepPurple': { + '200': 'b39ddb', + '900': '311b92', + '600': '5e35b1', + 'A100': 'b388ff', + '300': '9575cd', + 'A400': '651fff', + '700': '512da8', + '50': 'ede7f6', + 'A700': '6200EA', + '400': '7e57c2', + '100': 'd1c4e9', + '800': '4527a0', + 'A200': '7c4dff', + '500': '673ab7' + }, + 'Indigo': { + '200': '9fa8da', + '900': '1a237e', + '600': '3949ab', + 'A100': '8c9eff', + '300': '7986cb', + 'A400': '3d5afe', + '700': '303f9f', + '50': 'e8eaf6', + 'A700': '304ffe', + '400': '5c6bc0', + '100': 'c5cae9', + '800': '283593', + 'A200': '536dfe', + '500': '3f51b5' + }, + 'Blue': { + '200': '90caf9', + '900': '0D47A1', + '600': '1e88e5', + 'A100': '82b1ff', + '300': '64b5f6', + 'A400': '2979ff', + '700': '1976d2', + '50': 'e3f2fd', + 'A700': '2962ff', + '400': '42a5f5', + '100': 'bbdefb', + '800': '1565c0', + 'A200': '448aff', + '500': '2196f3' + }, + 'LightBlue': { + '200': '81d4fa', + '900': '01579B', + '600': '039BE5', + 'A100': '80d8ff', + '300': '4fc3f7', + 'A400': '00B0FF', + '700': '0288D1', + '50': 'e1f5fe', + 'A700': '0091EA', + '400': '29b6f6', + '100': 'b3e5fc', + '800': '0277BD', + 'A200': '40c4ff', + '500': '03A9F4' + }, + 'Cyan': { + '200': '80deea', + '900': '006064', + '600': '00ACC1', + 'A100': '84ffff', + '300': '4dd0e1', + 'A400': '00E5FF', + '700': '0097A7', + '50': 'e0f7fa', + 'A700': '00B8D4', + '400': '26c6da', + '100': 'b2ebf2', + '800': '00838F', + 'A200': '18ffff', + '500': '00BCD4' + }, + 'Teal': { + '200': '80cbc4', + '900': '004D40', + '600': '00897B', + 'A100': 'a7ffeb', + '300': '4db6ac', + 'A400': '1de9b6', + '700': '00796B', + '50': 'e0f2f1', + 'A700': '00BFA5', + '400': '26a69a', + '100': 'b2dfdb', + '800': '00695C', + 'A200': '64ffda', + '500': '009688' + }, + 'Green': { + '200': 'a5d6a7', + '900': '1b5e20', + '600': '43a047', + 'A100': 'b9f6ca', + '300': '81c784', + 'A400': '00E676', + '700': '388e3c', + '50': 'e8f5e9', + 'A700': '00C853', + '400': '66bb6a', + '100': 'c8e6c9', + '800': '2e7d32', + 'A200': '69f0ae', + '500': '4caf50' + }, + 'LightGreen': { + '200': 'c5e1a5', + '900': '33691e', + '600': '7cb342', + 'A100': 'ccff90', + '300': 'aed581', + 'A400': '76FF03', + '700': '689f38', + '50': 'f1f8e9', + 'A700': '64dd17', + '400': '9ccc65', + '100': 'dcedc8', + '800': '558b2f', + 'A200': 'b2ff59', + '500': '8bc34a' + }, + 'Lime': { + '200': 'e6ee9c', + '900': '827717', + '600': 'c0ca33', + 'A100': 'f4ff81', + '300': 'dce775', + 'A400': 'C6FF00', + '700': 'afb42b', + '50': 'f9fbe7', + 'A700': 'AEEA00', + '400': 'd4e157', + '100': 'f0f4c3', + '800': '9e9d24', + 'A200': 'eeff41', + '500': 'cddc39' + }, + 'Yellow': { + '200': 'fff59d', + '900': 'f57f17', + '600': 'fdd835', + 'A100': 'ffff8d', + '300': 'fff176', + 'A400': 'FFEA00', + '700': 'fbc02d', + '50': 'fffde7', + 'A700': 'FFD600', + '400': 'ffee58', + '100': 'fff9c4', + '800': 'f9a825', + 'A200': 'FFFF00', + '500': 'ffeb3b' + }, + 'Amber': { + '200': 'ffe082', + '900': 'FF6F00', + '600': 'FFB300', + 'A100': 'ffe57f', + '300': 'ffd54f', + 'A400': 'FFC400', + '700': 'FFA000', + '50': 'fff8e1', + 'A700': 'FFAB00', + '400': 'ffca28', + '100': 'ffecb3', + '800': 'FF8F00', + 'A200': 'ffd740', + '500': 'FFC107' + }, + 'Orange': { + '200': 'ffcc80', + '900': 'E65100', + '600': 'FB8C00', + 'A100': 'ffd180', + '300': 'ffb74d', + 'A400': 'FF9100', + '700': 'F57C00', + '50': 'fff3e0', + 'A700': 'FF6D00', + '400': 'ffa726', + '100': 'ffe0b2', + '800': 'EF6C00', + 'A200': 'ffab40', + '500': 'FF9800' + }, + 'DeepOrange': { + '200': 'ffab91', + '900': 'bf36c', + '600': 'f4511e', + 'A100': 'ff9e80', + '300': 'ff8a65', + 'A400': 'FF3D00', + '700': 'e64a19', + '50': 'fbe9e7', + 'A700': 'DD2C00', + '400': 'ff7043', + '100': 'ffccbc', + '800': 'd84315', + 'A200': 'ff6e40', + '500': 'ff5722' + }, + 'Brown': { + '200': 'bcaaa4', + '900': '3e2723', + '600': '6d4c41', + '300': 'a1887f', + '700': '5d4037', + '50': 'efebe9', + '400': '8d6e63', + '100': 'd7ccc8', + '800': '4e342e', + '500': '795548' + }, + 'Grey': { + '200': 'eeeeee', + '900': '212121', + '600': '757575', + '300': 'e0e0e0', + '700': '616161', + '50': 'fafafa', + '400': 'bdbdbd', + '100': 'f5f5f5', + '800': '424242', + '500': '9e9e9e' + }, + 'BlueGrey': { + '200': 'b0bec5', + '900': '263238', + '600': '546e7a', + '300': '90a4ae', + '700': '455a64', + '50': 'eceff1', + '400': '78909c', + '100': 'cfd8dc', + '800': '37474f', + '500': '607d8b' + }, + + 'Light': { + 'StatusBar': 'E0E0E0', + 'AppBar': 'F5F5F5', + 'Background': 'FAFAFA', + 'CardsDialogs': 'FFFFFF', + 'FlatButtonDown': 'cccccc' + }, + + 'Dark': { + 'StatusBar': '000000', + 'AppBar': '212121', + 'Background': '303030', + 'CardsDialogs': '424242', + 'FlatButtonDown': '999999' + } +} + +light_colors = { + 'Red': ['50', '100', '200', '300', 'A100'], + 'Pink': ['50', '100', '200', 'A100'], + 'Purple': ['50', '100', '200', 'A100'], + 'DeepPurple': ['50', '100', '200', 'A100'], + 'Indigo': ['50', '100', '200', 'A100'], + 'Blue': ['50','100', '200', '300', '400', 'A100'], + 'LightBlue': ['50', '100', '200', '300', '400', '500', 'A100', 'A200', + 'A400'], + 'Cyan': ['50', '100', '200', '300', '400', '500', '600', 'A100', 'A200', + 'A400', 'A700'], + 'Teal': ['50', '100', '200', '300', '400', 'A100', 'A200', 'A400', 'A700'], + 'Green': ['50', '100', '200', '300', '400', '500', 'A100', 'A200', 'A400', + 'A700'], + 'LightGreen': ['50', '100', '200', '300', '400', '500', '600', 'A100', + 'A200', 'A400', 'A700'], + 'Lime': ['50', '100', '200', '300', '400', '500', '600', '700', '800', + 'A100', 'A200', 'A400', 'A700'], + 'Yellow': ['50', '100', '200', '300', '400', '500', '600', '700', '800', + '900', 'A100', 'A200', 'A400', 'A700'], + 'Amber': ['50', '100', '200', '300', '400', '500', '600', '700', '800', + '900', 'A100', 'A200', 'A400', 'A700'], + 'Orange': ['50', '100', '200', '300', '400', '500', '600', '700', 'A100', + 'A200', 'A400', 'A700'], + 'DeepOrange': ['50', '100', '200', '300', '400', 'A100', 'A200'], + 'Brown': ['50', '100', '200'], + 'Grey': ['51', '100', '200', '300', '400', '500'], + 'BlueGrey': ['50', '100', '200', '300'], + 'Dark': [], + 'Light': ['White', 'MainBackground', 'DialogBackground'] +} + +level_bg_colors = [ + '50', + '100', + '200', + '300', +] + +level_selected_bg_colors = [ + '400', + '500', + '600', + '700' +] + +def getColors(style,level): + i = level % len(level_bg_colors) + text_color = text_colors[style][ level_bg_colors[i]] + bg_color = colors[style][ level_bg_colors[i]] + return color + +def getSelectedColors(style,level): + i = level % len(level_selected_bg_colors) + text_color = text_colors[style][ level_bg_colors[i]] + bg_color = colors[style][ level_bg_colors[i]] + return text_color, bg_color + +error_color_id = '900', +info_color_id = '800' +def getErrorColor(style): + text_color = text_colors[style][ error_color_id ] + bg_color = colors[style][ error_color_id ] + return text_color, bg_color + +def getInfoColor(style): + text_color = text_colors[style][ info_color_id ] + bg_color = colors[style][ info_color_id ] + return text_color, bg_color + + +text_colors = { + 'Red': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'Pink': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': 'ffffff', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'Purple': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': 'ffffff', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'DeepPurple': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': 'ffffff', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'Indigo': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': 'ffffff', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'Blue': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': 'ffffff', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'LightBlue': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': 'ffffff', + }, + 'Cyan': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Teal': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Green': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'LightGreen': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Lime': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': '000000', + '800': '000000', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Yellow': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': '000000', + '800': '000000', + '900': '000000', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Amber': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': '000000', + '800': '000000', + '900': '000000', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'Orange': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': '000000', + '700': '000000', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': '000000', + 'A700': '000000', + }, + 'DeepOrange': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + 'A100': '000000', + 'A200': '000000', + 'A400': 'ffffff', + 'A700': 'ffffff', + }, + 'Brown': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': 'ffffff', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + }, + 'Grey': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': '000000', + '500': '000000', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + }, + 'BlueGrey': { + '50': '000000', + '100': '000000', + '200': '000000', + '300': '000000', + '400': 'ffffff', + '500': 'ffffff', + '600': 'ffffff', + '700': 'ffffff', + '800': 'ffffff', + '900': 'ffffff', + }, +} diff --git a/kivyblocks/date_picker.py b/kivyblocks/date_picker.py new file mode 100644 index 0000000..4249f6f --- /dev/null +++ b/kivyblocks/date_picker.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +from kivy.lang import Builder +from kivy.uix.modalview import ModalView +from kivymd.label import MDLabel +from kivymd.theming import ThemableBehavior +from kivymd.backgroundcolorbehavior import SpecificBackgroundColorBehavior +from kivy.uix.floatlayout import FloatLayout +from kivymd.elevationbehavior import RectangularElevationBehavior +import calendar +from datetime import date +import datetime +from kivy.properties import StringProperty, NumericProperty, ObjectProperty, \ + BooleanProperty, ListProperty +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior +from kivymd.ripplebehavior import CircularRippleBehavior +from kivy.clock import Clock +from kivy.core.window import Window + +Builder.load_string(""" +#:import calendar calendar +#:import platform platform + + cal_layout: cal_layout + + size_hint: (None, None) + size: [dp(328), dp(484)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(512), dp(304)] + pos_hint: {'center_x': .5, 'center_y': .5} + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: [dp(328), dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(168), dp(304)] + pos: [root.pos[0], root.pos[1] + root.height-dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [root.pos[0], root.pos[1] + root.height-dp(304)] + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: [dp(328), dp(484)-dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(344), dp(304)] + pos: [root.pos[0], root.pos[1] + root.height-dp(96)-(dp(484)-dp(96))]\ + if self.theme_cls.device_orientation == 'portrait' else [root.pos[0]+dp(168), root.pos[1]] #+dp(334) + MDLabel: + id: label_full_date + font_style: 'Display1' + text_color: root.specific_text_color + theme_text_color: 'Custom' + size_hint: (None, None) + size: [root.width, dp(30)] if root.theme_cls.device_orientation == 'portrait'\ + else [dp(168), dp(30)] + pos: [root.pos[0]+dp(23), root.pos[1] + root.height - dp(74)] \ + if root.theme_cls.device_orientation == 'portrait' \ + else [root.pos[0]+dp(3), root.pos[1] + dp(214)] + line_height: 0.84 + valign: 'middle' + text_size: [root.width, None] if root.theme_cls.device_orientation == 'portrait'\ + else [dp(149), None] + bold: True + text: root.fmt_lbl_date(root.sel_year, root.sel_month, root.sel_day, root.theme_cls.device_orientation) + MDLabel: + id: label_year + font_style: 'Subhead' + text_color: root.specific_text_color + theme_text_color: 'Custom' + size_hint: (None, None) + size: root.width, dp(30) + pos: (root.pos[0]+dp(23), root.pos[1]+root.height-dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0]+dp(16), root.pos[1]+root.height-dp(41)) + valign: 'middle' + text: str(root.sel_year) + GridLayout: + id: cal_layout + cols: 7 + size: (dp(44*7), dp(40*7)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(46*7), dp(32*7)) + col_default_width: dp(42) if root.theme_cls.device_orientation == 'portrait'\ + else dp(39) + size_hint: (None, None) + padding: (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + spacing: (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + pos: (root.pos[0]+dp(10), root.pos[1]+dp(60)) if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0]+dp(168)+dp(8), root.pos[1]+dp(48)) + MDLabel: + id: label_month_selector + font_style: 'Body2' + text: calendar.month_name[root.month].capitalize() + ' ' + str(root.year) + size_hint: (None, None) + size: root.width, dp(30) + pos: root.pos + theme_text_color: 'Primary' + pos_hint: {'center_x': 0.5, 'center_y': 0.75} if self.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.67, 'center_y': 0.915} + valign: "middle" + halign: "center" + MDIconButton: + icon: 'chevron-left' + theme_text_color: 'Secondary' + pos_hint: {'center_x': 0.08, 'center_y': 0.745} if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.39, 'center_y': 0.925} + on_release: root.change_month('prev') + MDIconButton: + icon: 'chevron-right' + theme_text_color: 'Secondary' + pos_hint: {'center_x': 0.92, 'center_y': 0.745} if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.94, 'center_y': 0.925} + on_release: root.change_month('next') + MDFlatButton: + width: dp(32) + id: ok_button + pos: root.pos[0]+root.size[0]-self.width-dp(10), root.pos[1] + dp(10) + text: "OK" + on_release: root.ok_click() + MDFlatButton: + id: cancel_button + pos: root.pos[0]+root.size[0]-self.width-ok_button.width-dp(10), root.pos[1] + dp(10) + text: "Cancel" + on_release: root.dismiss() + + + size_hint: None, None + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + MDLabel: + font_style: 'Caption' + theme_text_color: 'Custom' if root.is_today and not root.is_selected else 'Primary' + text_color: root.theme_cls.primary_color + opposite_colors: root.is_selected if root.owner.sel_month == root.owner.month \ + and root.owner.sel_year == root.owner.year and str(self.text) == str(root.owner.sel_day) else False + size_hint_x: None + valign: 'middle' + halign: 'center' + text: root.text + + + font_style: 'Caption' + theme_text_color: 'Secondary' + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: None, None + text_size: self.size + valign: 'middle' if root.theme_cls.device_orientation == 'portrait' else 'bottom' + halign: 'center' + + + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: (None, None) + canvas: + Color: + rgba: self.theme_cls.primary_color if self.shown else [0, 0, 0, 0] + Ellipse: + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + pos: self.pos if root.theme_cls.device_orientation == 'portrait'\ + else [self.pos[0], self.pos[1]] +""") + + +class DaySelector(ThemableBehavior, AnchorLayout): + shown = BooleanProperty(False) + + def __init__(self, parent): + super(DaySelector, self).__init__() + self.parent_class = parent + self.parent_class.add_widget(self, index=7) + self.selected_widget = None + Window.bind(on_resize=self.move_resize) + + def update(self): + parent = self.parent_class + if parent.sel_month == parent.month and parent.sel_year == parent.year: + self.shown = True + else: + self.shown = False + + def set_widget(self, widget): + self.selected_widget = widget + self.pos = widget.pos + self.move_resize(do_again=True) + self.update() + + def move_resize(self, window=None, width=None, height=None, do_again=True): + self.pos = self.selected_widget.pos + if do_again: + Clock.schedule_once(lambda x: self.move_resize(do_again=False), 0.01) + + +class DayButton(ThemableBehavior, CircularRippleBehavior, ButtonBehavior, + AnchorLayout): + text = StringProperty() + owner = ObjectProperty() + is_today = BooleanProperty(False) + is_selected = BooleanProperty(False) + + def on_release(self): + self.owner.set_selected_widget(self) + + +class WeekdayLabel(MDLabel): + pass + + +class MDDatePicker(FloatLayout, ThemableBehavior, RectangularElevationBehavior, + SpecificBackgroundColorBehavior, ModalView): + _sel_day_widget = ObjectProperty() + cal_list = None + cal_layout = ObjectProperty() + sel_year = NumericProperty() + sel_month = NumericProperty() + sel_day = NumericProperty() + day = NumericProperty() + month = NumericProperty() + year = NumericProperty() + today = date.today() + callback = ObjectProperty() + background_color = ListProperty([0, 0, 0, 0.7]) + + class SetDateError(Exception): + pass + + def __init__(self, callback, year=None, month=None, day=None, + firstweekday=0, + **kwargs): + self.callback = callback + self.cal = calendar.Calendar(firstweekday) + self.sel_year = year if year else self.today.year + self.sel_month = month if month else self.today.month + self.sel_day = day if day else self.today.day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + super(MDDatePicker, self).__init__(**kwargs) + self.selector = DaySelector(parent=self) + self.generate_cal_widgets() + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def ok_click(self): + self.callback(date(self.sel_year, self.sel_month, self.sel_day)) + self.dismiss() + + def fmt_lbl_date(self, year, month, day, orientation): + d = datetime.date(int(year), int(month), int(day)) + separator = '\n' if orientation == 'landscape' else ' ' + return d.strftime('%a,').capitalize() + separator + d.strftime( + '%b').capitalize() + ' ' + str(day).lstrip('0') + + def set_date(self, year, month, day): + try: + date(year, month, day) + except Exception as e: + print(e) + if str(e) == "day is out of range for month": + raise self.SetDateError(" Day %s day is out of range for month %s" % (day, month)) + elif str(e) == "month must be in 1..12": + raise self.SetDateError("Month must be between 1 and 12, got %s" % month) + elif str(e) == "year is out of range": + raise self.SetDateError("Year must be between %s and %s, got %s" % + (datetime.MINYEAR, datetime.MAXYEAR, year)) + else: + self.sel_year = year + self.sel_month = month + self.sel_day = day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def set_selected_widget(self, widget): + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + widget.is_selected = True + self.sel_month = int(self.month) + self.sel_year = int(self.year) + self.sel_day = int(widget.text) + self._sel_day_widget = widget + self.selector.set_widget(widget) + + def set_month_day(self, day): + for idx in range(len(self.cal_list)): + if str(day) == str(self.cal_list[idx].text): + self._sel_day_widget = self.cal_list[idx] + self.sel_day = int(self.cal_list[idx].text) + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + self._sel_day_widget = self.cal_list[idx] + self.cal_list[idx].is_selected = True + self.selector.set_widget(self.cal_list[idx]) + + def update_cal_matrix(self, year, month): + try: + dates = [x for x in self.cal.itermonthdates(year, month)] + except ValueError as e: + if str(e) == "year is out of range": + pass + else: + self.year = year + self.month = month + for idx in range(len(self.cal_list)): + if idx >= len(dates) or dates[idx].month != month: + self.cal_list[idx].disabled = True + self.cal_list[idx].text = '' + else: + self.cal_list[idx].disabled = False + self.cal_list[idx].text = str(dates[idx].day) + self.cal_list[idx].is_today = dates[idx] == self.today + self.selector.update() + + def generate_cal_widgets(self): + cal_list = [] + for i in calendar.day_abbr: + self.cal_layout.add_widget(WeekdayLabel(text=i[0].upper())) + for i in range(6 * 7): # 6 weeks, 7 days a week + db = DayButton(owner=self) + cal_list.append(db) + self.cal_layout.add_widget(db) + self.cal_list = cal_list + + def change_month(self, operation): + op = 1 if operation is 'next' else -1 + sl, sy = self.month, self.year + m = 12 if sl + op == 0 else 1 if sl + op == 13 else sl + op + y = sy - 1 if sl + op == 0 else sy + 1 if sl + op == 13 else sy + self.update_cal_matrix(y, m) diff --git a/kivyblocks/dg.py b/kivyblocks/dg.py index 00a75e5..636477b 100644 --- a/kivyblocks/dg.py +++ b/kivyblocks/dg.py @@ -119,7 +119,6 @@ class Row(GridLayout): self.part.datagrid.select_rowid = self.row_id self.part.datagrid.dispatch('on_selected',self) - class Header(WidgetReady, ScrollWidget): def __init__(self,part,**kw): self.part = part diff --git a/kivyblocks/material_resources.py b/kivyblocks/material_resources.py new file mode 100644 index 0000000..6d3c43a --- /dev/null +++ b/kivyblocks/material_resources.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from kivy import platform +from kivy.core.window import Window +from kivy.metrics import dp +from kivyblocks import fonts_path + +# Feel free to override this const if you're designing for a device such as +# a GNU/Linux tablet. +if platform != "android" and platform != "ios": + DEVICE_TYPE = "desktop" +elif Window.width >= dp(600) and Window.height >= dp(600): + DEVICE_TYPE = "tablet" +else: + DEVICE_TYPE = "mobile" + +if DEVICE_TYPE == "mobile": + MAX_NAV_DRAWER_WIDTH = dp(300) + HORIZ_MARGINS = dp(16) + STANDARD_INCREMENT = dp(56) + PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT + LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT - dp(8) +else: + MAX_NAV_DRAWER_WIDTH = dp(400) + HORIZ_MARGINS = dp(24) + STANDARD_INCREMENT = dp(64) + PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT + LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT + +TOUCH_TARGET_HEIGHT = dp(48) + +FONTS = [ + { + "name": "Roboto", + "fn_regular": fonts_path + 'Roboto-Regular.ttf', + "fn_bold": fonts_path + 'Roboto-Medium.ttf', + "fn_italic": fonts_path + 'Roboto-Italic.ttf', + "fn_bolditalic": fonts_path + 'Roboto-MediumItalic.ttf' + }, + { + "name": "RobotoLight", + "fn_regular": fonts_path + 'Roboto-Thin.ttf', + "fn_bold": fonts_path + 'Roboto-Light.ttf', + "fn_italic": fonts_path + 'Roboto-ThinItalic.ttf', + "fn_bolditalic": fonts_path + 'Roboto-LightItalic.ttf' + }, + { + "name": "Icons", + "fn_regular": fonts_path + 'materialdesignicons-webfont.ttf' + } +] diff --git a/kivyblocks/theme_picker.py b/kivyblocks/theme_picker.py new file mode 100644 index 0000000..0c70f22 --- /dev/null +++ b/kivyblocks/theme_picker.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import OptionProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.modalview import ModalView +from kivy.utils import get_color_from_hex +from kivymd.backgroundcolorbehavior import SpecificBackgroundColorBehavior +from kivymd.button import MDIconButton +from kivymd.color_definitions import colors +from kivymd.elevationbehavior import RectangularElevationBehavior +from kivymd.theming import ThemableBehavior + +Builder.load_string(""" +#:import MDTabbedPanel kivymd.tabs.MDTabbedPanel +#:import MDTab kivymd.tabs.MDTab +: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex(root.color_name) + Ellipse: + size: self.size + pos: self.pos + +: + on_release: app.theme_cls.accent_palette = root.color_name + +: + on_release: app.theme_cls.primary_palette = root.color_name + +: + size_hint: (None, None) + size: dp(284), dp(120)+dp(290) + pos_hint: {'center_x': .5, 'center_y': .5} + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: self.width, dp(120) + pos: root.pos[0], root.pos[1] + root.height-dp(120) + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: self.width, dp(290) + pos: root.pos[0], root.pos[1] + root.height-(dp(120)+dp(290)) + + MDFlatButton: + pos: root.pos[0]+root.size[0]-self.width-dp(10), root.pos[1] + dp(10) + text: "Close" + on_release: root.dismiss() + MDLabel: + font_style: "Headline" + text: "Change theme" + size_hint: (None, None) + size: dp(160), dp(50) + pos_hint: {'center_x': 0.5, 'center_y': 0.9} + theme_text_color: 'Custom' + text_color: root.specific_text_color + MDTabbedPanel: + size_hint: (None, None) + size: root.width, root.height-dp(135) + pos_hint: {'center_x': 0.5, 'center_y': 0.475} + id: tab_panel + tab_display_mode:'text' + + MDTab: + name: 'color' + text: "Theme Color" + BoxLayout: + spacing: dp(4) + size_hint: (None, None) + size: dp(270), root.height # -dp(120) + pos_hint: {'center_x': 0.532, 'center_y': 0.89} + orientation: 'vertical' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + PrimaryColorSelector: + color_name: 'Red' + BoxLayout: + PrimaryColorSelector: + color_name: 'Pink' + BoxLayout: + PrimaryColorSelector: + color_name: 'Purple' + BoxLayout: + PrimaryColorSelector: + color_name: 'DeepPurple' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + PrimaryColorSelector: + color_name: 'Indigo' + BoxLayout: + PrimaryColorSelector: + color_name: 'Blue' + BoxLayout: + PrimaryColorSelector: + color_name: 'LightBlue' + BoxLayout: + PrimaryColorSelector: + color_name: 'Cyan' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + PrimaryColorSelector: + color_name: 'Teal' + BoxLayout: + PrimaryColorSelector: + color_name: 'Green' + BoxLayout: + PrimaryColorSelector: + color_name: 'LightGreen' + BoxLayout: + PrimaryColorSelector: + color_name: 'Lime' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + orientation: 'horizontal' + halign: 'center' + padding: 0, 0, 0, dp(1) + BoxLayout: + PrimaryColorSelector: + color_name: 'Yellow' + BoxLayout: + PrimaryColorSelector: + color_name: 'Amber' + BoxLayout: + PrimaryColorSelector: + color_name: 'Orange' + BoxLayout: + PrimaryColorSelector: + color_name: 'DeepOrange' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + #pos: self.pos + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + PrimaryColorSelector: + color_name: 'Brown' + BoxLayout: + PrimaryColorSelector: + color_name: 'Grey' + BoxLayout: + PrimaryColorSelector: + color_name: 'BlueGrey' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: app.theme_cls.bg_normal + Ellipse: + size: self.size + pos: self.pos + disabled: True + + MDTab: + name: 'accent_color' + text: "Accent Color" + BoxLayout: + spacing: dp(4) + size_hint: (None, None) + size: dp(270), root.height # -dp(120) + pos_hint: {'center_x': 0.532, 'center_y': 0.89} + orientation: 'vertical' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + AccentColorSelector: + color_name: 'Red' + BoxLayout: + AccentColorSelector: + color_name: 'Pink' + BoxLayout: + AccentColorSelector: + color_name: 'Purple' + BoxLayout: + AccentColorSelector: + color_name: 'DeepPurple' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + AccentColorSelector: + color_name: 'Indigo' + BoxLayout: + AccentColorSelector: + color_name: 'Blue' + BoxLayout: + AccentColorSelector: + color_name: 'LightBlue' + BoxLayout: + AccentColorSelector: + color_name: 'Cyan' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + AccentColorSelector: + color_name: 'Teal' + BoxLayout: + AccentColorSelector: + color_name: 'Green' + BoxLayout: + AccentColorSelector: + color_name: 'LightGreen' + BoxLayout: + AccentColorSelector: + color_name: 'Lime' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + orientation: 'horizontal' + halign: 'center' + padding: 0, 0, 0, dp(1) + BoxLayout: + AccentColorSelector: + color_name: 'Yellow' + BoxLayout: + AccentColorSelector: + color_name: 'Amber' + BoxLayout: + AccentColorSelector: + color_name: 'Orange' + BoxLayout: + AccentColorSelector: + color_name: 'DeepOrange' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + #pos: self.pos + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + AccentColorSelector: + color_name: 'Brown' + BoxLayout: + AccentColorSelector: + color_name: 'Grey' + BoxLayout: + AccentColorSelector: + color_name: 'BlueGrey' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: app.theme_cls.bg_normal + Ellipse: + size: self.size + pos: self.pos + disabled: True + + MDTab: + name: 'style' + text: "Theme Style" + FloatLayout: + size: self.size + pos: self.pos + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .6} + halign: 'center' + valign: 'center' + spacing: dp(10) + width: dp(210) + height: dp(100) + MDIconButton: + size: dp(100), dp(100) + size_hint: (None, None) + canvas: + Color: + rgba: 1, 1, 1, 1 + Ellipse: + size: self.size + pos: self.pos + Color: + rgba: 0, 0, 0, 1 + Line: + width: 1. + circle: (self.center_x, self.center_y, dp(50)) + on_release: app.theme_cls.theme_style = 'Light' + MDIconButton: + size: dp(100), dp(100) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: 0, 0, 0, 1 + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.theme_style = 'Dark' +""") + + +class ColorSelector(MDIconButton): + color_name = OptionProperty( + 'Indigo', + options=['Red', 'Pink', 'Purple', 'DeepPurple', 'Indigo', 'Blue', + 'LightBlue', 'Cyan', 'Teal', 'Green', 'LightGreen', + 'Lime', 'Yellow', 'Amber', 'Orange', 'DeepOrange', + 'Brown', 'Grey', 'BlueGrey']) + + def rgb_hex(self, col): + return get_color_from_hex(colors[col][self.theme_cls.accent_hue]) + + +class MDThemePicker(ThemableBehavior, FloatLayout, ModalView, + SpecificBackgroundColorBehavior, + RectangularElevationBehavior): + pass + + +if __name__ == "__main__": + from kivy.app import App + from kivymd.theming import ThemeManager + + class ThemePickerApp(App): + theme_cls = ThemeManager() + + def build(self): + main_widget = Builder.load_string(""" +#:import MDRaisedButton kivymd.button.MDRaisedButton +#:import MDThemePicker kivymd.theme_picker.MDThemePicker +FloatLayout: + MDRaisedButton: + size_hint: None, None + pos_hint: {'center_x': .5, 'center_y': .5} + size: 3 * dp(48), dp(48) + center_x: self.parent.center_x + text: 'Open theme picker' + on_release: MDThemePicker().open() + opposite_colors: True +""") + return main_widget + + ThemePickerApp().run() diff --git a/kivyblocks/theming.py b/kivyblocks/theming.py new file mode 100644 index 0000000..b0d1342 --- /dev/null +++ b/kivyblocks/theming.py @@ -0,0 +1,351 @@ +# -*- coding: utf-8 -*- +from kivy.app import App +from kivy.core.text import LabelBase +from kivy.core.window import Window +from kivy.clock import Clock +from kivy.metrics import dp +from kivy.properties import OptionProperty, AliasProperty, ObjectProperty, \ + StringProperty, ListProperty, BooleanProperty +from kivy.uix.widget import Widget +from kivy.utils import get_color_from_hex +from kivy.atlas import Atlas +from kivyblocks.color_definitions import colors, text_colors +from kivyblocks.material_resources import FONTS, DEVICE_TYPE +from kivyblocks import images_path + +for font in FONTS: + LabelBase.register(**font) + + +class ThemeManager(Widget): + primary_palette = OptionProperty( + 'Blue', + options=['Pink', 'Blue', 'Indigo', 'BlueGrey', 'Brown', + 'LightBlue', + 'Purple', 'Grey', 'Yellow', 'LightGreen', 'DeepOrange', + 'Green', 'Red', 'Teal', 'Orange', 'Cyan', 'Amber', + 'DeepPurple', 'Lime']) + + primary_hue = OptionProperty( + '500', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + primary_light_hue = OptionProperty( + '200', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + primary_dark_hue = OptionProperty( + '700', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + def _get_primary_color(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_hue]) + + primary_color = AliasProperty(_get_primary_color, + bind=('primary_palette', 'primary_hue')) + + def _get_primary_light(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_light_hue]) + + primary_light = AliasProperty( + _get_primary_light, bind=('primary_palette', 'primary_light_hue')) + + def _get_primary_dark(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_dark_hue]) + + primary_dark = AliasProperty(_get_primary_dark, + bind=('primary_palette', 'primary_dark_hue')) + + accent_palette = OptionProperty( + 'Amber', + options=['Pink', 'Blue', 'Indigo', 'BlueGrey', 'Brown', + 'LightBlue', + 'Purple', 'Grey', 'Yellow', 'LightGreen', 'DeepOrange', + 'Green', 'Red', 'Teal', 'Orange', 'Cyan', 'Amber', + 'DeepPurple', 'Lime']) + + accent_hue = OptionProperty( + '500', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + accent_light_hue = OptionProperty( + '200', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + accent_dark_hue = OptionProperty( + '700', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + def _get_accent_color(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_hue]) + + accent_color = AliasProperty(_get_accent_color, + bind=['accent_palette', 'accent_hue']) + + def _get_accent_light(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_light_hue]) + + accent_light = AliasProperty(_get_accent_light, + bind=['accent_palette', 'accent_light_hue']) + + def _get_accent_dark(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_dark_hue]) + + accent_dark = AliasProperty(_get_accent_dark, + bind=['accent_palette', 'accent_dark_hue']) + + theme_style = OptionProperty('Light', options=['Light', 'Dark']) + + def _get_theme_style(self, opposite): + if opposite: + return 'Light' if self.theme_style == 'Dark' else 'Dark' + else: + return self.theme_style + + def _get_bg_darkest(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['StatusBar']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['StatusBar']) + + bg_darkest = AliasProperty(_get_bg_darkest, bind=['theme_style']) + + def _get_op_bg_darkest(self): + return self._get_bg_darkest(True) + + opposite_bg_darkest = AliasProperty(_get_op_bg_darkest, + bind=['theme_style']) + + def _get_bg_dark(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['AppBar']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['AppBar']) + + bg_dark = AliasProperty(_get_bg_dark, bind=['theme_style']) + + def _get_op_bg_dark(self): + return self._get_bg_dark(True) + + opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=['theme_style']) + + def _get_bg_normal(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['Background']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['Background']) + + bg_normal = AliasProperty(_get_bg_normal, bind=['theme_style']) + + def _get_op_bg_normal(self): + return self._get_bg_normal(True) + + opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=['theme_style']) + + def _get_bg_light(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['CardsDialogs']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['CardsDialogs']) + + bg_light = AliasProperty(_get_bg_light, bind=['theme_style']) + + def _get_op_bg_light(self): + return self._get_bg_light(True) + + opposite_bg_light = AliasProperty(_get_op_bg_light, bind=['theme_style']) + + def _get_divider_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .12 + return color + + divider_color = AliasProperty(_get_divider_color, bind=['theme_style']) + + def _get_op_divider_color(self): + return self._get_divider_color(True) + + opposite_divider_color = AliasProperty(_get_op_divider_color, + bind=['theme_style']) + + def _get_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .87 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + return color + + text_color = AliasProperty(_get_text_color, bind=['theme_style']) + + def _get_op_text_color(self): + return self._get_text_color(True) + + opposite_text_color = AliasProperty(_get_op_text_color, + bind=['theme_style']) + + def _get_secondary_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .54 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .70 + return color + + secondary_text_color = AliasProperty(_get_secondary_text_color, + bind=['theme_style']) + + def _get_op_secondary_text_color(self): + return self._get_secondary_text_color(True) + + opposite_secondary_text_color = AliasProperty(_get_op_secondary_text_color, + bind=['theme_style']) + + def _get_icon_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .54 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + return color + + icon_color = AliasProperty(_get_icon_color, + bind=['theme_style']) + + def _get_op_icon_color(self): + return self._get_icon_color(True) + + opposite_icon_color = AliasProperty(_get_op_icon_color, + bind=['theme_style']) + + def _get_disabled_hint_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .38 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .50 + return color + + disabled_hint_text_color = AliasProperty(_get_disabled_hint_text_color, + bind=['theme_style']) + + def _get_op_disabled_hint_text_color(self): + return self._get_disabled_hint_text_color(True) + + opposite_disabled_hint_text_color = AliasProperty( + _get_op_disabled_hint_text_color, bind=['theme_style']) + + # Hardcoded because muh standard + def _get_error_color(self): + return get_color_from_hex(colors['Red']['A700']) + + error_color = AliasProperty(_get_error_color) + + + def _get_ripple_color(self): + return self._ripple_color + + def _set_ripple_color(self, value): + self._ripple_color = value + + _ripple_color = ListProperty(get_color_from_hex(colors['Grey']['400'])) + ripple_color = AliasProperty(_get_ripple_color, + _set_ripple_color, + bind=['_ripple_color']) + + def _determine_device_orientation(self, _, window_size): + if window_size[0] > window_size[1]: + self.device_orientation = 'landscape' + elif window_size[1] >= window_size[0]: + self.device_orientation = 'portrait' + + device_orientation = StringProperty('') + + def _get_standard_increment(self): + if DEVICE_TYPE == 'mobile': + if self.device_orientation == 'landscape': + return dp(48) + else: + return dp(56) + else: + return dp(64) + + standard_increment = AliasProperty(_get_standard_increment, + bind=['device_orientation']) + + def _get_horizontal_margins(self): + if DEVICE_TYPE == 'mobile': + return dp(16) + else: + return dp(24) + + horizontal_margins = AliasProperty(_get_horizontal_margins) + + def on_theme_style(self, instance, value): + if hasattr(App.get_running_app(), 'theme_cls') and \ + App.get_running_app().theme_cls == self: + self.set_clearcolor_by_theme_style(value) + + def set_clearcolor_by_theme_style(self, theme_style): + if theme_style == 'Light': + Window.clearcolor = get_color_from_hex( + colors['Light']['Background']) + elif theme_style == 'Dark': + Window.clearcolor = get_color_from_hex( + colors['Dark']['Background']) + + def __init__(self, **kwargs): + super(ThemeManager, self).__init__(**kwargs) + self.rec_shadow = Atlas('{}rec_shadow.atlas'.format(images_path)) + self.rec_st_shadow = Atlas('{}rec_st_shadow.atlas'.format(images_path)) + self.quad_shadow = Atlas('{}quad_shadow.atlas'.format(images_path)) + self.round_shadow = Atlas('{}round_shadow.atlas'.format(images_path)) + Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style)) + self._determine_device_orientation(None, Window.size) + Window.bind(size=self._determine_device_orientation) + + +class ThemableBehavior(object): + theme_cls = ObjectProperty(None) + opposite_colors = BooleanProperty(False) + + def __init__(self, **kwargs): + if self.theme_cls is not None: + pass + elif hasattr(App.get_running_app(), 'theme_cls'): + self.theme_cls = App.get_running_app().theme_cls + else: + self.theme_cls = ThemeManager() + super(ThemableBehavior, self).__init__(**kwargs) diff --git a/kivyblocks/theming_dynamic_text.py b/kivyblocks/theming_dynamic_text.py new file mode 100644 index 0000000..76d34f6 --- /dev/null +++ b/kivyblocks/theming_dynamic_text.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Two implementations. The first is based on color brightness obtained from:- +# https://www.w3.org/TR/AERT#color-contrast +# The second is based on relative luminance calculation for sRGB obtained from:- +# https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef +# and contrast ratio calculation obtained from:- +# https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef +# +# Preliminary testing suggests color brightness more closely matches the +# Material Design spec suggested text colors, but the alternative implementation +# is both newer and the current 'correct' recommendation, so is included here +# as an option. + + +def _color_brightness(color): + # Implementation of color brightness method + brightness = color[0] * 299 + color[1] * 587 + color[2] * 114 + brightness = brightness + return brightness + + +def _black_or_white_by_color_brightness(color): + if _color_brightness(color) >= 500: + return 'black' + else: + return 'white' + + +def _normalized_channel(color): + # Implementation of contrast ratio and relative luminance method + if color <= 0.03928: + return color / 12.92 + else: + return ((color + 0.055) / 1.055) ** 2.4 + + +def _luminance(color): + rg = _normalized_channel(color[0]) + gg = _normalized_channel(color[1]) + bg = _normalized_channel(color[2]) + return 0.2126*rg + 0.7152*gg + 0.0722*bg + + +def _black_or_white_by_contrast_ratio(color): + l_color = _luminance(color) + l_black = 0.0 + l_white = 1.0 + b_contrast = (l_color + 0.05) / (l_black + 0.05) + w_contrast = (l_white + 0.05) / (l_color + 0.05) + return 'white' if w_contrast >= b_contrast else 'black' + + +def get_contrast_text_color(color, use_color_brightness=True): + if use_color_brightness: + contrast_color = _black_or_white_by_color_brightness(color) + else: + contrast_color = _black_or_white_by_contrast_ratio(color) + if contrast_color == 'white': + return 1, 1, 1, 1 + else: + return 0, 0, 0, 1 + +if __name__ == '__main__': + from kivy.utils import get_color_from_hex + from kivymd.color_definitions import colors, text_colors + for c in colors.items(): + if c[0] in ['Light', 'Dark']: + continue + print("For the {} color palette:".format(c[0])) + for name, hex_color in c[1].items(): + if hex_color: + col = get_color_from_hex(hex_color) + col_bri = get_contrast_text_color(col) + con_rat = get_contrast_text_color(col, use_color_brightness=False) + text_color = text_colors[c[0]][name] + print(" The {} hue gives {} using color brightness, {} using contrast ratio, and {} from the MD spec" + .format(name, col_bri, con_rat, text_color)) diff --git a/kivyblocks/tree.py b/kivyblocks/tree.py new file mode 100644 index 0000000..dfc3ef9 --- /dev/null +++ b/kivyblocks/tree.py @@ -0,0 +1,245 @@ +from kivy.app import App +from kivy.graphics import Color, Triangle +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label +from kivy.uix.widget import Widget +from kivy.uix.button import ButtonBehavior + +from kivyblocks.widgetExt import ScrollWidget +from kivyblocks.blocksapp import BlocksApp +from kivyblocks.utils import CSize +from baseWidget import PressableLabel + +from appPublic.dictObject import DictObject + +class NodeTrigger(ButtonBehavior, Widget): + def __init__(self, open_status=False,color=[1,0,0,1]): + siz = CSize(1) + super().__init__(size_hint=(None,None),size=[siz,siz]) #CSize(1)) + self.open_status = open_status + self.line_color = color + self.register_event_type('on_change') + self.open_points = [0,self.height, self.width,self.height,self.width/2,0] + self.close_points = [0,self.height, 0,0, self.width,self.height/2] + self.showOpenStatus() + self.bind(size=self.on_size) + + def on_change(self,status): + print('new_status=',status) + + def on_size(self,o,v): + self.open_points = [0,self.height, self.width,self.height,self.width/2,0] + self.close_points = [0,self.height, 0,0, self.width,self.height/2] + + def on_press(self): + self.open_status = False if self.open_status else True + self.showOpenStatus() + self.dispatch('on_change',self.open_status) + + def showOpenStatus(self): + points = self.close_points + if self.open_status: + points = self.open_points + self.canvas.clear() + with self.canvas: + Color(*self.line_color) + Triangle(points=points) + +class EmptyBox(Widget): + def __init__(self): + siz=CSize(1) + super().__init__(size_hint=[None,None],size=[siz,siz]) + + +class TreeNode(BoxLayout): + def __init__(self,tree:Tree=None, + parentNode:TreeNode=None, + data:dict): + """ + base TreeNode + data:{ + if children miss, it is a leaf node,if children is a empty array, is mean need load at it is first expanded. + } + """ + super().__init__(orientation='vertical',size_hint=(None,None)) + self.treeObj = tree + self.data = data + self.parentNonde = parentNode + self.content = None + self.children_open = False + self.subNodes = [] + self.node_box = BoxLayout(orientation='horizontal',size_hint_y=None) + self.node_box1 = BoxLayout(orientation='horizontal') + n = data.get('children') + self.hasChildren_nodes = True if not n is None else False + self.children_nodes = n + self.children_loaded = False + self.hasChildren = data.get('hasChildren') + if hasChildren_nodes: + self.trigger = NodeTrigger() + self.trigger.bind(on_change=self.toggleChildren) + self.buildChildrenContainer() + else: + self.trigger = EmptyBox() + self.add_widget(self.node_box) + self.node_box.add_widget(self.trigger) + self.buildContent() + + def buildChildrenContainer(self): + self.node_box1.add_widget(EmptyBox()) + self.children_box = BoxLayout(orientation='vertical') + self.node_box1.add_widget(self.children_box) + + def setMinWidth(self,width): + if self.node_box.width < width: + self.node_box.width = width + if self.node_box1.width < width: + self.node_box1.width = width + for n in self.subNodes: + n.setMinWidth(width) + + def buildContent(self): + pass + + def addContent(self): + self.buildContent() + self.node_box.add_widget(self.content) + self.node_box.height = self.content.height + CSize(1) + self.node_box.width = self.trigger.width + self.content.width + + def buildChildren(self): + if self.data.children is None: + return + + if self.data.children == []: + self.treeObj.getUrlData(self.addChildren,self.data) + return + if len(self.subNodes) == 0: + self.addChildren(self.data.children) + + def addChildren(self,children): + self.data.children = children + for c in children: + options['tree'] = self.treeObj + options['parentNode'] = self + options['data'] = c + tn = self.teeeObj.NodeKlass(**options) + self.subNodes.append(tn) + self.children_box.add_widget(tn) + + def toggleChildren(self,o,v): + if v: + self.buildChildren() + self.add_widget(self.node_box1) + self.children_open = True + self.height = self.node_box.height + self.node_box1.height + else: + self.remove_widget(self.node_box1) + self.height = self.node_box.height + self.children_open = False + + def on_size(self,o,v): + print('************treenode on_size', o, v) + self.node_box1.height = self.children_box.height + +""" +tree options +{ + "url": + "params", + "bg_color", + "color", + "checkbox", + "multplecheck", + "idField", + "textFiled", + "data" # array of {children:{},...} +} +""" +class Tree(ScrollWidget): + def __init__(self,**options): + super().__init__() #orientation="vertical") + self.options = DictObject(**options) + self.nodes = [] + self.initflag = False + self.bind(size=self.onSize,pos=self.onSize) + + def onSize(self,o,v=None): + if not self.initflag: + self.buildTree(self) + for n in self.nodes: + n.setMinWidth(self.width) + + def setNodeKlass(self,klass): + self.NodeKlass = klass + + def getUrlData(self,callback,kv=None): + hc = App.get_running_app().hc + params = self.options.get(params,{}).copy() + if not data is None: + for k,v in kv: + if k in params.keys(): + params[k] = v + params['id'] = kv[self.options.idField] + hc.get(self.url,params=params,callback=callback) + + def buildTree(self,kv=None): + if not hasattr(self,'NodeKlass'): + self.NodeKlass = TreeNode + + if self.options.url: + return self.getUrlData(self.dataLoaded,kv) + data = self.options.data + self.dataLoaded(data) + + def dataLoaded(self,d): + self.data = d + self.addNodes() + + def addNodes(self): + for c in self.data: + options = {} + options['tree'] = self + options['parentNode'] = None + options['data'] = DictObject(c) + self.nodes.append(self.NodeKlass(**options)) + for w in self.nodes: + self.add_widget(w) + +class TextTreeNode(TreeNode): + def buildContent(self): + txt = self.data.get(self.treeObj.options.textField, + self.data.get(self.treeObj.options.idField,'defaulttext')) + self.content = PressableLabel(text=txt, + size_hint=(None,None), + font_size=CSize(1), + text_size=CSize(len(txt),1), + halign='left', + height=CSize(2),width=CSize(len(txt))) + self.content.bind(on_press=self.onPress) + return + + def onPress(self,o,v=None): + self.treeObj.select_row(self) + + def selected(self): + pass + + def unselected(self): + pass + +class TextTree(Tree): + def __init__(self,**options): + self.NodeKlass = TextTReeNode + super().__init__(**options) + self.register_event_type('on_press') + + def onPress(self,o,v=None): + if self.selectNode: + self.selectNode.unselected() + self.selectNode = o + o.selected() + self.dispatch(on_press,o,o.data) + + def on_press(self,o,v): + print('TextTree():on_press(),o=',o,'v=',v)