diff --git a/doc/sources/guide/environment.rst b/doc/sources/guide/environment.rst index ff7d12409..b98f4bb2b 100644 --- a/doc/sources/guide/environment.rst +++ b/doc/sources/guide/environment.rst @@ -32,6 +32,11 @@ KIVY_NO_FILELOG KIVY_NO_CONSOLELOG If set, logs will be not print on the console +KIVY_DPI + If set, the value will be used instead of the value returned by the window. + + .. versionadded:: 1.4.0 + Path control ------------ @@ -95,3 +100,4 @@ KIVY_CLIPBOARD Implementation to use for clipboard management Values: pygame, dummy + diff --git a/kivy/__init__.py b/kivy/__init__.py index 2a6a013e8..b82419f20 100644 --- a/kivy/__init__.py +++ b/kivy/__init__.py @@ -171,6 +171,8 @@ def kivy_usage(): Save current Kivy configuration. --size=640x480 Size of window geometry. + --dpi=96 + Manually overload the Window DPI (for testing only.) ''' print kivy_usage.__doc__ % (basename(sys.argv[0])) @@ -284,7 +286,8 @@ if not environ.get('KIVY_DOC_INCLUDE'): opts, args = getopt(sys_argv[1:], 'hp:fkawFem:sr:dc:', ['help', 'fullscreen', 'windowed', 'fps', 'event', 'module=', 'save', 'fake-fullscreen', 'auto-fullscreen', - 'display=', 'size=', 'rotate=', 'config=', 'debug']) + 'display=', 'size=', 'rotate=', 'config=', 'debug', + 'dpi=']) except GetoptError, err: Logger.error('Core: %s' % str(err)) @@ -353,6 +356,8 @@ if not environ.get('KIVY_DOC_INCLUDE'): elif opt in ('-d', '--debug'): level = LOG_LEVELS.get('debug') Logger.setLevel(level=level) + elif opt == '--dpi': + environ['KIVY_DPI'] = arg if need_save and 'KIVY_NO_CONFIG' not in environ: try: diff --git a/kivy/base.py b/kivy/base.py index d9aa1cf2f..1374d4eb3 100644 --- a/kivy/base.py +++ b/kivy/base.py @@ -15,10 +15,12 @@ __all__ = ( 'stopTouchApp', ) +from os import environ from kivy.config import Config from kivy.logger import Logger from kivy.clock import Clock from kivy.event import EventDispatcher +from kivy.utils import platform, reify # private vars EventLoop = None @@ -103,6 +105,32 @@ class EventLoopBase(EventDispatcher): ''' return self.me_list + @reify + def dpi(self): + '''Return the DPI of the screen. Depending of the platform, the DPI can + be taken from the Window provider (Desktop mainly), or from + platform-specific module (like android/ios). + + On desktop, you can overload the value returned by the Window object + (96 by default), by setting the environ KIVY_DPI:: + + KIVY_DPI=200 python main.py + + .. versionadded:: 1.4.0 + ''' + custom_dpi = environ.get('KIVY_DPI') + if custom_dpi: + return float(custom_dpi) + + plat = platform() + if plat == 'android': + import android + return android.get_dpi() + + # for all other platforms.. + self.ensure_window() + return self.window.dpi + def ensure_window(self): '''Ensure that we have an window ''' diff --git a/kivy/core/text/text_pil.py b/kivy/core/text/text_pil.py index 252599ae5..886839179 100644 --- a/kivy/core/text/text_pil.py +++ b/kivy/core/text/text_pil.py @@ -20,7 +20,7 @@ class LabelPIL(LabelBase): _cache = {} def _select_font(self): - fontsize = int(self.options['font_size'] * 1.333) + fontsize = int(self.options['font_size']) fontname = self.options['font_name_r'] id = '%s.%s' % (unicode(fontname), unicode(fontsize)) if not id in self._cache: diff --git a/kivy/core/text/text_pygame.py b/kivy/core/text/text_pygame.py index d01e3a707..85323f242 100644 --- a/kivy/core/text/text_pygame.py +++ b/kivy/core/text/text_pygame.py @@ -35,7 +35,7 @@ class LabelPygame(LabelBase): if ext.lower() == 'ttf': # fontobject fontobject = pygame.font.Font(fontname, - int(self.options['font_size'] * 1.333)) + int(self.options['font_size'])) # fallback to search a system font if fontobject is None: @@ -47,7 +47,7 @@ class LabelPygame(LabelBase): # fontobject fontobject = pygame.font.Font(font, - int(self.options['font_size'] * 1.333)) + int(self.options['font_size'])) pygame_cache[fontid] = fontobject pygame_cache_order.append(fontid) diff --git a/kivy/core/window/__init__.py b/kivy/core/window/__init__.py index 833fc3021..0ad21777d 100755 --- a/kivy/core/window/__init__.py +++ b/kivy/core/window/__init__.py @@ -21,7 +21,7 @@ from kivy.modules import Modules from kivy.event import EventDispatcher from kivy.properties import ListProperty, ObjectProperty, AliasProperty, \ NumericProperty, OptionProperty, StringProperty -from kivy.utils import platform +from kivy.utils import platform, reify # late import VKeyboard = None @@ -804,6 +804,18 @@ class WindowBase(EventDispatcher): ''' pass + @reify + def dpi(self): + '''Return the DPI of the screen. If the implementation doesn't support + any DPI lookup, it will just return 96. + + .. warning:: + + This value is not cross-platform. Use + :data:`kivy.base.EventLoop.dpi` instead. + ''' + return 96. + def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) diff --git a/kivy/properties.pxd b/kivy/properties.pxd index 47ba06247..f8ae80270 100644 --- a/kivy/properties.pxd +++ b/kivy/properties.pxd @@ -2,7 +2,7 @@ cdef class Property: cdef str _name cdef int allownone cdef object defaultvalue - cdef init_storage(self, dict storage) + cdef init_storage(self, object obj, dict storage) cpdef link(self, object obj, str name) cpdef link_deps(self, object obj, str name) cpdef bind(self, obj, observer) @@ -15,7 +15,8 @@ cdef class Property: cpdef dispatch(self, obj) cdef class NumericProperty(Property): - pass + cdef float parse_str(self, object obj, value) + cdef float parse_list(self, object obj, value, bytes ext) cdef class StringProperty(Property): pass diff --git a/kivy/properties.pyx b/kivy/properties.pyx index 39713ed6a..f07c53b08 100644 --- a/kivy/properties.pyx +++ b/kivy/properties.pyx @@ -153,6 +153,9 @@ __all__ = ('Property', from weakref import ref +EventLoop = None + + cdef class Property: '''Base class for building more complex properties. @@ -197,8 +200,8 @@ cdef class Property: def __get__(self): return self._name - cdef init_storage(self, dict storage): - storage['value'] = self.defaultvalue + cdef init_storage(self, object obj, dict storage): + storage['value'] = self.convert(obj, self.defaultvalue) storage['allownone'] = self.allownone storage['observers'] = [] @@ -223,8 +226,8 @@ cdef class Property: ''' d = dict() self._name = name - self.init_storage(d) obj.__storage[name] = d + self.init_storage(obj, d) cpdef link_deps(self, object obj, str name): pass @@ -327,27 +330,87 @@ cdef class NumericProperty(Property): The NumericProperty accepts only int or float. - >>> Widget.x = 42 - >>> print Widget.x + >>> wid = Widget() + >>> wid.x = 42 + >>> print wid.x 42 - >>> Widget.x = "plop" - Traceback (most recent call last): - File "", line 1, in - File "properties.pyx", line 93, in kivy.properties.Property.__set__ - File "properties.pyx", line 111, in kivy.properties.Property.set - File "properties.pyx", line 159, in kivy.properties.NumericProperty.check + >>> wid.x = "plop" + Traceback (most recent call last): + File "", line 1, in + File "properties.pyx", line 93, in kivy.properties.Property.__set__ + File "properties.pyx", line 111, in kivy.properties.Property.set + File "properties.pyx", line 159, in kivy.properties.NumericProperty.check ValueError: NumericProperty accept only int/float + + .. versionchanged:: 1.4.1 + NumericProperty can now accept custom text and tuple value to indicate a + type, like "in", "pt", "px", "cm", "mm", in the format: '10pt' or (10, + 'pt'). + ''' def __init__(self, defaultvalue=0, **kw): super(NumericProperty, self).__init__(defaultvalue, **kw) + cdef init_storage(self, object obj, dict storage): + storage['format'] = 'px' + Property.init_storage(self, obj, storage) + cdef check(self, obj, value): if Property.check(self, obj, value): return True if type(value) not in (int, float): - raise ValueError('%s.%s accept only int/float' % ( + raise ValueError('%s.%s accept only int/float (got %r)' % ( obj.__class__.__name__, - self.name)) + self.name, value)) + + cdef convert(self, obj, x): + if x is None: + return x + tp = type(x) + if tp is int or tp is float: + return x + if tp is tuple or tp is list: + if len(x) != 2: + raise ValueError('%s.%s must have 2 components (got %r)' % ( + obj.__class__.__name__, + self.name, x)) + return self.parse_list(obj, x[0], x[1]) + elif tp is str: + return self.parse_str(obj, x) + else: + raise ValueError('%s.%s have an invalid format (got %r)' % ( + obj.__class__.__name__, + self.name, x)) + + cdef float parse_str(self, object obj, value): + return self.parse_list(obj, value[:-2], value[-2:]) + + cdef float parse_list(self, object obj, value, bytes ext): + # 1in = 2.54cm = 25.4mm = 72pt = 12pc + global EventLoop + if EventLoop is None: + from kivy.base import EventLoop + cdef float rv = float(value) + cdef float dpi = EventLoop.dpi + obj.__storage[self.name]['format'] = ext + if ext == 'in': + return rv * dpi + elif ext == 'px': + return rv + elif ext == 'pt': + return rv * dpi / 72. + elif ext == 'cm': + return rv * dpi / 2.54 + elif ext == 'mm': + return rv * dpi / 25.4 + + def get_format(self, obj): + ''' + Return the format used for Numeric calculation. Default is px (mean + the value have not been changed at all). Otherwise, it can be one of + 'in', 'pt', 'cm', 'mm'. + ''' + return obj.__storage[self.name]['format'] cdef class StringProperty(Property): @@ -601,8 +664,8 @@ cdef class BoundedNumericProperty(Property): self.max = value Property.__init__(self, *largs, **kw) - cdef init_storage(self, dict storage): - Property.init_storage(self, storage) + cdef init_storage(self, object obj, dict storage): + Property.init_storage(self, obj, storage) storage['min'] = self.min storage['max'] = self.max storage['use_min'] = self.use_min @@ -727,8 +790,8 @@ cdef class OptionProperty(Property): self.options = (kw.get('options', [])) super(OptionProperty, self).__init__(*largs, **kw) - cdef init_storage(self, dict storage): - Property.init_storage(self, storage) + cdef init_storage(self, object obj, dict storage): + Property.init_storage(self, obj, storage) storage['options'] = self.options[:] cdef check(self, obj, value): @@ -769,8 +832,8 @@ cdef class ReferenceListProperty(Property): self.properties.append(prop) Property.__init__(self, largs, **kw) - cdef init_storage(self, dict storage): - Property.init_storage(self, storage) + cdef init_storage(self, object obj, dict storage): + Property.init_storage(self, obj, storage) storage['properties'] = self.properties storage['stop_event'] = 0 @@ -869,8 +932,8 @@ cdef class AliasProperty(Property): self.bind_objects = list(v) if v is not None else [] self.use_cache = 1 if kwargs.get('cache') else 0 - cdef init_storage(self, dict storage): - Property.init_storage(self, storage) + cdef init_storage(self, object obj, dict storage): + Property.init_storage(self, obj, storage) storage['getter'] = self.getter storage['setter'] = self.setter storage['initial'] = True diff --git a/kivy/uix/label.py b/kivy/uix/label.py index 211363941..90ca51954 100644 --- a/kivy/uix/label.py +++ b/kivy/uix/label.py @@ -148,6 +148,9 @@ class Label(Widget): # markup have change, we need to change our rendering method. d = Label._font_properties dkw = dict(zip(d, [getattr(self, x) for x in d])) + # XXX font_size core provider compatibility + if Label.font_size.get_format(self) == 'px': + dkw['font_size'] *= 1.333 if markup: self._label = CoreMarkupLabel(**dkw) else: @@ -162,6 +165,11 @@ class Label(Widget): self._label.text = value elif name == 'text_size': self._label.usersize = value + elif name == 'font_size': + # XXX font_size core provider compatibility + if Label.font_size.get_format(self) == 'px': + value *= 1.333 + self._label.options[name] = value else: self._label.options[name] = value self._trigger_texture() @@ -266,7 +274,7 @@ class Label(Widget): 'DroidSans'. ''' - font_size = NumericProperty(12) + font_size = NumericProperty('12px') '''Font size of the text, in pixels. :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, default to diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py index c6dfbb75c..3839076c3 100644 --- a/kivy/uix/textinput.py +++ b/kivy/uix/textinput.py @@ -1074,9 +1074,15 @@ class TextInput(Widget): def _get_line_options(self): # Get or create line options, to be used for Label creation + + # XXX font_size core label compatibility + factor = 1. + if TextInput.font_size.get_format(self) == 'px': + factor = 1.333 + if self._line_options is None: self._line_options = kw = { - 'font_size': self.font_size, + 'font_size': self.font_size * factor, 'font_name': self.font_name, 'anchor_x': 'left', 'anchor_y': 'top', diff --git a/kivy/utils.py b/kivy/utils.py index e43742293..1130db85c 100644 --- a/kivy/utils.py +++ b/kivy/utils.py @@ -10,7 +10,7 @@ __all__ = ('intersection', 'difference', 'strtotuple', 'is_color_transparent', 'boundary', 'deprecated', 'SafeList', 'interpolate', 'OrderedDict', 'QueryDict', - 'platform', 'escape_markup') + 'platform', 'escape_markup', 'reify') from sys import platform as _sys_platform from re import match, split @@ -370,3 +370,28 @@ def escape_markup(text): ''' return text.replace('[', '&bl;').replace(']', '&br;').replace('&', '&') + +class reify(object): + ''' + Put the result of a method which uses this (non-data) descriptor decorator + in the instance dict after the first call, effectively replacing the + decorator with an instance variable. + + It acts like @property, except that the function is only ever called once; + after that, the value is cached as a regular attribute. This gives you lazy + attribute creation on objects that are meant to be immutable. + + Taken from Pyramid project. + ''' + + def __init__(self, func): + self.func = func + self.__doc__ = func.__doc__ + + def __get__(self, inst, cls): + if inst is None: + return self + retval = self.func(inst) + setattr(inst, self.func.__name__, retval) + return retval +