diff --git a/kivyblocks/custom_camera.py b/kivyblocks/custom_camera.py index c99df63..1e29bb1 100644 --- a/kivyblocks/custom_camera.py +++ b/kivyblocks/custom_camera.py @@ -1,3 +1,4 @@ +from kivy.logger import Logger from kivy.uix.camera import Camera from kivy.properties import BooleanProperty, NumericProperty from kivy.uix.button import Button @@ -61,10 +62,12 @@ class CustomCamera(Camera): class QrReader(Camera): def __init__(self, **kw): + Logger.info('QrReader:Initialed...........') super(QrReader, self).__init__(**kw) self.qr_reader = cv2.QRCodeDetector() self.register_event_type('on_data') self.qr_result = None + Logger.info('QrReader:Initialed') def getValue(self): return { diff --git a/kivyblocks/markdown.py b/kivyblocks/markdown.py index 40f3953..564933c 100644 --- a/kivyblocks/markdown.py +++ b/kivyblocks/markdown.py @@ -222,7 +222,7 @@ class MarkdownBody(VBox): def build_source(self,source_desc): w = MarkdownBody(md_obj=self.md_obj, - csscls=md_obj.second_css, size_hint_y=None) + csscls=self.md_obj.second_css, size_hint_y=None) w.show_mdtext(source_desc) self.add_widget(w) w.resize() @@ -396,18 +396,18 @@ description file format first_css = StringProperty("default") second_css = StringProperty("default") def __init__(self, **kw): - ScrollView.__init__(self) + super(Markdown, self).__init__(**kw) self.root_body = MarkdownBody(md_obj=self, csscls=self.first_css, size_hint_y=None ) self.root_body.bind(minimum_height=self.root_body.setter('height')) self.add_widget(self.root_body) + self.source = kw.get('source') if self.source: self.load_text() self.bind(size=self.root_body.resize) self.bind(source=self.load_text) - # Clock.schedule_interval(self.check_parent_window, 1) def check_parent_window(self, *args): pw = self.get_parent_window() @@ -418,7 +418,7 @@ description file format self.root_body.stp_video() def load_text(self, *args): - h = getDataHandler(self.source) + h = getDataHandler(self.source) h.bind(on_success=self.update) h.bind(on_error=self.show_error) h.handle() diff --git a/kivyblocks/pagepanel.py b/kivyblocks/pagepanel.py index 296d2bb..ec02b0f 100644 --- a/kivyblocks/pagepanel.py +++ b/kivyblocks/pagepanel.py @@ -300,14 +300,17 @@ sub-widget's description file format self.right_menu_showed = False print('dismiss fired, right_menu_showed=',self.right_menu_showed) - print('right fired') + print('right menu fired') if len(self.sub_widgets) < 1: + print('no sub_widgets') return if self.right_menu_showed: + print('right menu showed, not thing done') return w = self.sub_widgets[-1] if not hasattr(w, 'menu_widget'): + print('this sub widget has not menu_widget') return True mc = MenuContainer() mc.add_widget(w.menu_widget) diff --git a/kivyblocks/qrcodereader.py b/kivyblocks/qrcodereader.py index 81d86ad..cb99aaa 100644 --- a/kivyblocks/qrcodereader.py +++ b/kivyblocks/qrcodereader.py @@ -1,17 +1,20 @@ import numpy as np import cv2 from kivy.app import App +from kivy.properties import BooleanProperty from kivy.core.window import Window from kivy.graphics.texture import Texture from kivy.uix.image import Image from kivy.clock import Clock class QRCodeReader(Image): + opened = BooleanProperty(False) def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type('on_data') - self.opened = False self.task = None + if self.opened: + self.open() def open(self): self.opened = True diff --git a/kivyblocks/xcamera/__init__.py b/kivyblocks/xcamera/__init__.py new file mode 100644 index 0000000..e2c5931 --- /dev/null +++ b/kivyblocks/xcamera/__init__.py @@ -0,0 +1,15 @@ +""" +Exposes `XCamera` directly in `xcamera` rather than `xcamera.xcamera`. +Also note this may break `pip` since all imports within `xcamera.py` would be +required at setup time. This is because `version.py` (same directory) is used +by the `setup.py` file. +Hence we're not exposing `XCamera` if `pip` is detected. +""" +import os + +project_dir = os.path.abspath( + os.path.join(__file__, os.pardir, os.pardir, os.pardir, os.pardir)) +using_pip = os.path.basename(project_dir).startswith('pip-') +# only exposes `XCamera` if not within `pip` ongoing install +if not using_pip: + from .xcamera import XCamera # noqa diff --git a/kivyblocks/xcamera/android_api.py b/kivyblocks/xcamera/android_api.py new file mode 100644 index 0000000..1beb5b8 --- /dev/null +++ b/kivyblocks/xcamera/android_api.py @@ -0,0 +1,85 @@ +from kivy.logger import Logger + +from jnius import JavaException, PythonJavaClass, autoclass, java_method + +Camera = autoclass('android.hardware.Camera') +AndroidActivityInfo = autoclass('android.content.pm.ActivityInfo') +AndroidPythonActivity = autoclass('org.kivy.android.PythonActivity') +PORTRAIT = AndroidActivityInfo.SCREEN_ORIENTATION_PORTRAIT +LANDSCAPE = AndroidActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + + +class ShutterCallback(PythonJavaClass): + __javainterfaces__ = ('android.hardware.Camera$ShutterCallback', ) + + @java_method('()V') + def onShutter(self): + # apparently, it is enough to have an empty shutter callback to play + # the standard shutter sound. If you pass None instead of shutter_cb + # below, the standard sound doesn't play O_o + pass + + +class PictureCallback(PythonJavaClass): + __javainterfaces__ = ('android.hardware.Camera$PictureCallback', ) + + def __init__(self, filename, on_success): + super(PictureCallback, self).__init__() + self.filename = filename + self.on_success = on_success + + @java_method('([BLandroid/hardware/Camera;)V') + def onPictureTaken(self, data, camera): + s = data.tostring() + with open(self.filename, 'wb') as f: + f.write(s) + Logger.info('xcamera: picture saved to %s', self.filename) + camera.startPreview() + self.on_success(self.filename) + + +class AutoFocusCallback(PythonJavaClass): + __javainterfaces__ = ('android.hardware.Camera$AutoFocusCallback', ) + + def __init__(self, filename, on_success): + super(AutoFocusCallback, self).__init__() + self.filename = filename + self.on_success = on_success + + @java_method('(ZLandroid/hardware/Camera;)V') + def onAutoFocus(self, success, camera): + if success: + Logger.info('xcamera: autofocus succeeded, taking picture...') + shutter_cb = ShutterCallback() + picture_cb = PictureCallback(self.filename, self.on_success) + camera.takePicture(shutter_cb, None, picture_cb) + else: + Logger.info('xcamera: autofocus failed') + + +def take_picture(camera_widget, filename, on_success): + # to call the android API, we need access to the underlying + # android.hardware.Camera instance. However, there is no official way to + # retrieve it from the camera widget, so we need to dig into internal + # attributes :-( This works at least on kivy 1.9.1, but it might break any + # time soon. + camera = camera_widget._camera._android_camera + params = camera.getParameters() + params.setFocusMode("auto") + camera.setParameters(params) + cb = AutoFocusCallback(filename, on_success) + Logger.info('xcamera: starting autofocus...') + try: + camera.autoFocus(cb) + except JavaException as e: + Logger.info('Error when calling autofocus: {}'.format(e)) + + +def set_orientation(value): + previous = get_orientation() + AndroidPythonActivity.mActivity.setRequestedOrientation(value) + return previous + + +def get_orientation(): + return AndroidPythonActivity.mActivity.getRequestedOrientation() diff --git a/kivyblocks/xcamera/data/icons.ttf b/kivyblocks/xcamera/data/icons.ttf new file mode 100644 index 0000000..1eae4aa Binary files /dev/null and b/kivyblocks/xcamera/data/icons.ttf differ diff --git a/kivyblocks/xcamera/data/shutter.wav b/kivyblocks/xcamera/data/shutter.wav new file mode 100644 index 0000000..66d4380 Binary files /dev/null and b/kivyblocks/xcamera/data/shutter.wav differ diff --git a/kivyblocks/xcamera/main.py b/kivyblocks/xcamera/main.py new file mode 100644 index 0000000..c522662 --- /dev/null +++ b/kivyblocks/xcamera/main.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +from kivy.app import App +from kivy.lang import Builder + +kv = """ +#:import XCamera kivy_garden.xcamera.XCamera + +FloatLayout: + orientation: 'vertical' + + XCamera: + id: xcamera + on_picture_taken: app.picture_taken(*args) + + BoxLayout: + orientation: 'horizontal' + size_hint: 1, None + height: sp(50) + + Button: + text: 'Set landscape' + on_release: xcamera.force_landscape() + + Button: + text: 'Restore orientation' + on_release: xcamera.restore_orientation() +""" + + +class CameraApp(App): + def build(self): + return Builder.load_string(kv) + + def picture_taken(self, obj, filename): + print('Picture taken and saved to {}'.format(filename)) + + +def main(): + CameraApp().run() + + +if __name__ == '__main__': + main() diff --git a/kivyblocks/xcamera/platform_api.py b/kivyblocks/xcamera/platform_api.py new file mode 100644 index 0000000..ea0236d --- /dev/null +++ b/kivyblocks/xcamera/platform_api.py @@ -0,0 +1,37 @@ +from kivy.utils import platform + + +def play_shutter(): + # bah, apparently we need to delay the import of kivy.core.audio, lese + # kivy cannot find a camera provider, at lease on linux. Maybe a + # gstreamer/pygame issue? + from kivy.core.audio import SoundLoader + sound = SoundLoader.load("data/shutter.wav") + sound.play() + + +if platform == 'android': + from .android_api import ( + LANDSCAPE, PORTRAIT, take_picture, set_orientation, get_orientation) + +else: + + # generic fallback for taking pictures. Probably not the best quality, + # they are meant mostly for testing + LANDSCAPE = 'landscape' + PORTRAIT = 'portrait' + + def take_picture(camera_widget, filename, on_success): + camera_widget.texture.save(filename, flipped=False) + play_shutter() + on_success(filename) + + def set_orientation(value): + previous = get_orientation() + print('FAKE orientation set to {}'.format(value)) + get_orientation.value = value + return previous + + def get_orientation(): + return get_orientation.value + get_orientation.value = PORTRAIT diff --git a/kivyblocks/xcamera/version.py b/kivyblocks/xcamera/version.py new file mode 100644 index 0000000..51ac59e --- /dev/null +++ b/kivyblocks/xcamera/version.py @@ -0,0 +1 @@ +__version__ = '2020.0613' diff --git a/kivyblocks/xcamera/xcamera.kv b/kivyblocks/xcamera/xcamera.kv new file mode 100644 index 0000000..b8af343 --- /dev/null +++ b/kivyblocks/xcamera/xcamera.kv @@ -0,0 +1,41 @@ +#:import xcamera kivy_garden.xcamera.xcamera + + + icon_color: (0, 0, 0, 1) + _down_color: xcamera.darker(self.icon_color) + icon_size: dp(50) + + canvas.before: + Color: + rgba: self.icon_color if self.state == 'normal' else self._down_color + Ellipse: + pos: self.pos + size: self.size + + size_hint: None, None + size: self.icon_size, self.icon_size + font_size: self.icon_size/2 + + +: + # \ue800 corresponds to the camera icon in the font + icon: u"[font=data/icons.ttf]\ue800[/font]" + icon_color: (0.13, 0.58, 0.95, 0.8) + icon_size: dp(70) + + id: camera + resolution: 640, 480 # 1920, 1080 + allow_stretch: True + + # Shoot button + XCameraIconButton: + id: shoot_button + markup: True + text: root.icon + icon_color: root.icon_color + icon_size: root.icon_size + on_release: root.shoot() + + # position + right: root.width - dp(10) + center_y: root.center_y diff --git a/kivyblocks/xcamera/xcamera.py b/kivyblocks/xcamera/xcamera.py new file mode 100644 index 0000000..f08871d --- /dev/null +++ b/kivyblocks/xcamera/xcamera.py @@ -0,0 +1,116 @@ +import datetime +import os + +from kivy.clock import mainthread +from kivy.lang import Builder +from kivy.properties import ObjectProperty +from kivy.resources import resource_add_path +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.camera import Camera +from kivy.uix.label import Label +from kivy.utils import platform + +from .platform_api import LANDSCAPE, set_orientation, take_picture + +ROOT = os.path.dirname(os.path.abspath(__file__)) +resource_add_path(ROOT) + + +def darker(color, factor=0.5): + r, g, b, a = color + r *= factor + g *= factor + b *= factor + return r, g, b, a + + +def get_filename(): + return datetime.datetime.now().strftime('%Y-%m-%d %H.%M.%S.jpg') + + +def is_android(): + return platform == 'android' + + +def check_camera_permission(): + """ + Android runtime `CAMERA` permission check. + """ + if not is_android(): + return True + from android.permissions import Permission, check_permission + permission = Permission.CAMERA + return check_permission(permission) + + +def check_request_camera_permission(callback=None): + """ + Android runtime `CAMERA` permission check & request. + """ + had_permission = check_camera_permission() + if not had_permission: + from android.permissions import Permission, request_permissions + permissions = [Permission.CAMERA] + request_permissions(permissions, callback) + return had_permission + + +class XCameraIconButton(ButtonBehavior, Label): + pass + + +class XCamera(Camera): + directory = ObjectProperty(None) + _previous_orientation = None + __events__ = ('on_picture_taken', 'on_camera_ready') + + def __init__(self, **kwargs): + Builder.load_file(os.path.join(ROOT, "xcamera.kv")) + super().__init__(**kwargs) + + def _on_index(self, *largs): + """ + Overrides `kivy.uix.camera.Camera._on_index()` to make sure + `camera.open()` is not called unless Android `CAMERA` permission is + granted, refs #5. + """ + @mainthread + def on_permissions_callback(permissions, grant_results): + """ + On camera permission callback calls parent `_on_index()` method. + """ + if all(grant_results): + self._on_index_dispatch(*largs) + if check_request_camera_permission(callback=on_permissions_callback): + self._on_index_dispatch(*largs) + + def _on_index_dispatch(self, *largs): + super()._on_index(*largs) + self.dispatch('on_camera_ready') + + def on_picture_taken(self, filename): + """ + This event is fired every time a picture has been taken. + """ + pass + + def on_camera_ready(self): + """ + Fired when the camera is ready. + """ + pass + + def shoot(self): + def on_success(filename): + self.dispatch('on_picture_taken', filename) + filename = get_filename() + if self.directory: + filename = os.path.join(self.directory, filename) + take_picture(self, filename, on_success) + + def force_landscape(self): + self._previous_orientation = set_orientation(LANDSCAPE) + + def restore_orientation(self): + if self._previous_orientation is not None: + set_orientation(self._previous_orientation)