├── presemt ├── icon.png ├── data │ ├── add.png │ ├── eye.png │ ├── edit.png │ ├── image.png │ ├── info.png │ ├── left.png │ ├── lock.png │ ├── right.png │ ├── save.png │ ├── text.png │ ├── trash.png │ ├── refresh.png │ ├── selector.png │ ├── text-add.png │ ├── text-del.png │ ├── highlight.png │ ├── home_alpha.png │ ├── thumbs-up.png │ ├── toolbar_left.png │ ├── toolbar_right.png │ └── transparent.png ├── help │ ├── main_0.jpg │ ├── main_1.jpg │ ├── main_2.jpg │ ├── player_0.jpg │ ├── selector_0.jpg │ ├── selector_1.jpg │ └── sources │ │ ├── main_0.xcf │ │ ├── main_1.xcf │ │ ├── main_2.xcf │ │ ├── player_0.xcf │ │ ├── selector_0.xcf │ │ └── selector_1.xcf ├── screens │ ├── loading.py │ ├── __init__.py │ ├── presentation_panel.py │ ├── presentation_objects.py │ ├── presentation_slides.py │ ├── project.py │ ├── presentation_plane.py │ └── presentation.py ├── config.py ├── fbocapture.py ├── behaviours.py ├── main.py ├── document.py └── presemt.kv ├── .gitignore ├── README └── COPYING /presemt/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/icon.png -------------------------------------------------------------------------------- /presemt/data/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/add.png -------------------------------------------------------------------------------- /presemt/data/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/eye.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.pyc 3 | *.pyo 4 | *~ 5 | *.swp 6 | *.DS_Store 7 | *.kpf 8 | build/* 9 | -------------------------------------------------------------------------------- /presemt/data/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/edit.png -------------------------------------------------------------------------------- /presemt/data/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/image.png -------------------------------------------------------------------------------- /presemt/data/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/info.png -------------------------------------------------------------------------------- /presemt/data/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/left.png -------------------------------------------------------------------------------- /presemt/data/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/lock.png -------------------------------------------------------------------------------- /presemt/data/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/right.png -------------------------------------------------------------------------------- /presemt/data/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/save.png -------------------------------------------------------------------------------- /presemt/data/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/text.png -------------------------------------------------------------------------------- /presemt/data/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/trash.png -------------------------------------------------------------------------------- /presemt/data/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/refresh.png -------------------------------------------------------------------------------- /presemt/data/selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/selector.png -------------------------------------------------------------------------------- /presemt/data/text-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/text-add.png -------------------------------------------------------------------------------- /presemt/data/text-del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/text-del.png -------------------------------------------------------------------------------- /presemt/help/main_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/main_0.jpg -------------------------------------------------------------------------------- /presemt/help/main_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/main_1.jpg -------------------------------------------------------------------------------- /presemt/help/main_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/main_2.jpg -------------------------------------------------------------------------------- /presemt/help/player_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/player_0.jpg -------------------------------------------------------------------------------- /presemt/data/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/highlight.png -------------------------------------------------------------------------------- /presemt/data/home_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/home_alpha.png -------------------------------------------------------------------------------- /presemt/data/thumbs-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/thumbs-up.png -------------------------------------------------------------------------------- /presemt/help/selector_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/selector_0.jpg -------------------------------------------------------------------------------- /presemt/help/selector_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/selector_1.jpg -------------------------------------------------------------------------------- /presemt/screens/loading.py: -------------------------------------------------------------------------------- 1 | from . import Screen 2 | 3 | class LoadingScreen(Screen): 4 | pass 5 | -------------------------------------------------------------------------------- /presemt/data/toolbar_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/toolbar_left.png -------------------------------------------------------------------------------- /presemt/data/toolbar_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/toolbar_right.png -------------------------------------------------------------------------------- /presemt/data/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/data/transparent.png -------------------------------------------------------------------------------- /presemt/help/sources/main_0.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/main_0.xcf -------------------------------------------------------------------------------- /presemt/help/sources/main_1.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/main_1.xcf -------------------------------------------------------------------------------- /presemt/help/sources/main_2.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/main_2.xcf -------------------------------------------------------------------------------- /presemt/help/sources/player_0.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/player_0.xcf -------------------------------------------------------------------------------- /presemt/help/sources/selector_0.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/selector_0.xcf -------------------------------------------------------------------------------- /presemt/help/sources/selector_1.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tito/presemt/HEAD/presemt/help/sources/selector_1.xcf -------------------------------------------------------------------------------- /presemt/screens/__init__.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.floatlayout import FloatLayout 2 | 3 | class Screen(FloatLayout): 4 | def __init__(self, **kwargs): 5 | self.app = kwargs.get('app') 6 | super(Screen, self).__init__(**kwargs) 7 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | PreseMT 2 | ======= 3 | 4 | How to run ? 5 | ------------ 6 | 7 | 1. Download Kivy at http://kivy.org/, and follow installation instructions 8 | 2. Execute the presemt/main.py file to run the application 9 | 10 | Thanks 11 | ------ 12 | 13 | We would like to thanks Thomas Hansen for his original idea and implementation 14 | with PyMT. 15 | 16 | -------------------------------------------------------------------------------- /presemt/config.py: -------------------------------------------------------------------------------- 1 | 2 | from kivy.core.image import ImageLoader 3 | 4 | SUPPORTED_IMG = [] 5 | for loader in ImageLoader.loaders: 6 | for ext in loader.extensions(): 7 | if ext not in SUPPORTED_IMG: 8 | SUPPORTED_IMG.append(ext) 9 | # OK, who has a better idea on how to do that that is still acceptable? 10 | SUPPORTED_VID = ['avi', 'mpg', 'mpeg'] 11 | 12 | -------------------------------------------------------------------------------- /presemt/fbocapture.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.floatlayout import FloatLayout 2 | from kivy.properties import ObjectProperty, ListProperty 3 | from kivy.graphics import Rectangle, Fbo, Color, Canvas 4 | from kivy.factory import Factory 5 | 6 | 7 | class FboCapture(FloatLayout): 8 | 9 | texture = ObjectProperty(None) 10 | 11 | texture_thumb = ObjectProperty(None) 12 | 13 | thumb_size = ListProperty([50, 50]) 14 | 15 | def __init__(self, **kwargs): 16 | self.canvas = Canvas() 17 | with self.canvas: 18 | self.fbo = Fbo(size=self.size) 19 | self.fbo_thumb = Fbo(size=self.thumb_size) 20 | with self.fbo: 21 | Color(0, 0, 0) 22 | self.fbo_rect = Rectangle(size=self.size) 23 | self.texture = self.fbo.texture 24 | with self.fbo_thumb: 25 | Color(1, 1, 1) 26 | self.fbo_thumb_rect = Rectangle(size=self.thumb_size) 27 | super(FboCapture, self).__init__(**kwargs) 28 | 29 | def on_size(self, instance, value): 30 | w, h = value 31 | ratio = float(w) / h 32 | if w > h: 33 | w = 160 34 | h = w / ratio 35 | else: 36 | h = 160 37 | w = h * ratio 38 | w = max(1, w) 39 | h = max(1, h) 40 | self.thumb_size = int(w), int(h) 41 | self.fbo.size = value 42 | self.fbo_rect.size = value 43 | self.texture = self.fbo.texture 44 | self.fbo_thumb_rect.texture = self.fbo.texture 45 | 46 | def on_thumb_size(self, instance, value): 47 | self.fbo_thumb.size = value 48 | self.fbo_thumb_rect.size = value 49 | self.texture_thumb = self.fbo_thumb.texture 50 | 51 | def add_widget(self, child): 52 | child.parent = self 53 | self.children.insert(0, child) 54 | self.fbo.add(child.canvas) 55 | 56 | def remove_widget(self, child): 57 | self.children.remove(child) 58 | self.fbo.remove(child.canvas) 59 | 60 | Factory.register('FboCapture', cls=FboCapture) 61 | -------------------------------------------------------------------------------- /presemt/screens/presentation_panel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Panel for configuration 3 | Panel have open() and close() hook, to know when they are displayed or not 4 | ''' 5 | 6 | from kivy.uix.floatlayout import FloatLayout 7 | from kivy.properties import ObjectProperty, StringProperty, ListProperty 8 | from kivy.factory import Factory 9 | 10 | from config import SUPPORTED_VID, SUPPORTED_IMG 11 | 12 | try: 13 | import android 14 | user_path = u'/sdcard' 15 | except ImportError: 16 | user_path = u'~' 17 | 18 | def prefix(exts): 19 | return ['*.' + t for t in exts] 20 | 21 | 22 | class Panel(FloatLayout): 23 | 24 | ctrl = ObjectProperty(None) 25 | 26 | def __init__(self, **kwargs): 27 | self.register_event_type('on_open') 28 | self.register_event_type('on_close') 29 | super(Panel, self).__init__(**kwargs) 30 | 31 | def on_open(self): 32 | pass 33 | 34 | def on_close(self): 35 | pass 36 | 37 | 38 | class TextStackEntry(Factory.BoxLayout): 39 | 40 | panel = ObjectProperty(None) 41 | 42 | text = StringProperty('') 43 | 44 | ctrl = ObjectProperty(None) 45 | 46 | def on_touch_down(self, touch): 47 | if super(TextStackEntry, self).on_touch_down(touch): 48 | return True 49 | if self.collide_point(*touch.pos): 50 | self.ctrl.create_text(touch=touch, text=self.text) 51 | return True 52 | 53 | Factory.register('TextStackEntry', cls=TextStackEntry) 54 | 55 | class ImageButton(Factory.ButtonBehavior, Factory.Image): 56 | pass 57 | 58 | Factory.register('ImageButton', cls=ImageButton) 59 | 60 | class TextPanel(Panel): 61 | 62 | textinput = ObjectProperty(None) 63 | 64 | stack = ObjectProperty(None) 65 | 66 | status_btn = 'btn_panel_text' 67 | 68 | def add_text(self): 69 | text = self.textinput.text.strip() 70 | self.textinput.text = '' 71 | self.textinput.focus = False 72 | if not text: 73 | return 74 | label = TextStackEntry(text=text, ctrl=self.ctrl, panel=self) 75 | self.stack.add_widget(label) 76 | self.ctrl.create_text(text=text) 77 | 78 | 79 | class LocalFilePanel(Panel): 80 | 81 | path = StringProperty(user_path) 82 | 83 | status_btn = 'btn_panel_image' 84 | 85 | imgtypes = ListProperty(prefix(SUPPORTED_IMG)) 86 | 87 | vidtypes = ListProperty(prefix(SUPPORTED_VID)) 88 | 89 | suptypes = ListProperty(prefix(SUPPORTED_IMG + SUPPORTED_VID)) 90 | 91 | 92 | -------------------------------------------------------------------------------- /presemt/screens/presentation_objects.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Objects that will be added on the plane 3 | ''' 4 | 5 | from kivy.uix.scatter import Scatter 6 | from kivy.properties import BooleanProperty, ObjectProperty, \ 7 | StringProperty, ListProperty, NumericProperty 8 | 9 | class PlaneObject(Scatter): 10 | 11 | selected = BooleanProperty(False) 12 | 13 | ctrl = ObjectProperty(None) 14 | 15 | def __init__(self, **kwargs): 16 | super(PlaneObject, self).__init__(**kwargs) 17 | touch = kwargs.get('touch_follow', None) 18 | if touch: 19 | touch.ud.scatter_follow = self 20 | touch.grab(self) 21 | self.bind(transform=self._on_transform) 22 | 23 | def _on_transform(self, instance, value): 24 | if self.ctrl: 25 | self.ctrl.set_dirty() 26 | 27 | def collide_point(self, x, y): 28 | x, y = self.to_local(x, y) 29 | w2 = self.width / 2. 30 | h2 = self.height / 2. 31 | return -w2 <= x <= w2 and -h2 <= y <= h2 32 | 33 | def on_touch_down(self, touch): 34 | if self.collide_point(*touch.pos): 35 | if touch.is_double_tap: 36 | self.ctrl.remove_object(self) 37 | return True 38 | else: 39 | self.ctrl.configure_object(self) 40 | return super(PlaneObject, self).on_touch_down(touch) 41 | 42 | def on_touch_move(self, touch): 43 | if touch.grab_current is self: 44 | if 'scatter_follow' in touch.ud: 45 | self.pos = touch.pos 46 | return super(PlaneObject, self).on_touch_move(touch) 47 | 48 | 49 | class TextPlaneObject(PlaneObject): 50 | 51 | text = StringProperty('Hello world') 52 | 53 | bold = BooleanProperty(False) 54 | 55 | italic = BooleanProperty(False) 56 | 57 | color = ListProperty([1, 1, 1, 1]) 58 | 59 | font_name = StringProperty(None) 60 | 61 | font_size = NumericProperty(96) 62 | 63 | 64 | class MediaPlaneObject(PlaneObject): 65 | 66 | source = StringProperty(None) 67 | 68 | do_adjust = BooleanProperty(False) 69 | 70 | def __init__(self, **kwargs): 71 | super(MediaPlaneObject, self).__init__(**kwargs) 72 | 73 | def on_size(self, instance, value): 74 | if not self.do_adjust: 75 | return 76 | self.scale = min(1, 1. / (max(1, self.width, self.height) / (640. / self.ctrl.plane.scale))) 77 | 78 | 79 | 80 | class ImagePlaneObject(MediaPlaneObject): 81 | pass 82 | 83 | class VideoPlaneObject(MediaPlaneObject): 84 | pass 85 | 86 | -------------------------------------------------------------------------------- /presemt/screens/presentation_slides.py: -------------------------------------------------------------------------------- 1 | from kivy.factory import Factory 2 | from kivy.properties import ObjectProperty, NumericProperty, \ 3 | ListProperty, BooleanProperty 4 | from kivy.graphics import Fbo, Rectangle, Color 5 | from kivy.graphics.opengl import glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE 6 | 7 | 8 | class Slide(Factory.ButtonBehavior, Factory.Image): 9 | 10 | ctrl = ObjectProperty(None) 11 | 12 | slide_rotation = NumericProperty(0) 13 | 14 | slide_scale = NumericProperty(1.) 15 | 16 | slide_pos = ListProperty([0,0]) 17 | 18 | selected = BooleanProperty(False) 19 | 20 | index = NumericProperty(0) 21 | 22 | def __init__(self, **kwargs): 23 | # get raw rgb thumb is available 24 | self.thumb = kwargs.get('thumb', None) 25 | del kwargs['thumb'] 26 | # extract controler now, we need it. 27 | self.ctrl = kwargs.get('ctrl') 28 | # create fbo for tiny texture 29 | self.fbo = Fbo(size=(160, 120)) 30 | with self.fbo: 31 | Color(1, 1, 1) 32 | Rectangle(size=self.fbo.size) 33 | self.fborect = Rectangle(size=self.fbo.size) 34 | if self.thumb: 35 | self.upload_thumb() 36 | else: 37 | self.update_capture() 38 | super(Slide, self).__init__(**kwargs) 39 | 40 | def on_press(self, touch): 41 | if touch.is_double_tap: 42 | self.ctrl.remove_slide(self) 43 | else: 44 | self.ctrl.select_slide(self) 45 | 46 | def update_capture(self, *largs): 47 | edit_mode = self.ctrl.is_edit 48 | self.ctrl.is_edit = False 49 | 50 | # update main fbo 51 | fbo = self.ctrl.capture.fbo 52 | fbo.ask_update() 53 | fbo.draw() 54 | 55 | # update our tiny fbo 56 | self.fborect.texture = fbo.texture 57 | self.fbo.ask_update() 58 | self.fbo.draw() 59 | 60 | # then bind the texture to our texture image 61 | self.texture = self.fbo.texture 62 | self.texture_size = self.texture.size 63 | 64 | self.ctrl.is_edit = edit_mode 65 | self.ctrl.set_dirty() 66 | self.thumb = None 67 | 68 | def download_thumb(self): 69 | if self.thumb is None: 70 | fbo = self.fbo 71 | fbo.draw() 72 | fbo.bind() 73 | tmp = glReadPixels(0, 0, fbo.size[0], fbo.size[1], GL_RGBA, GL_UNSIGNED_BYTE) 74 | fbo.release() 75 | # remove alpha 76 | tmp = list(tmp) 77 | del tmp[3::4] 78 | tmp = ''.join(tmp) 79 | self.thumb = (fbo.size[0], fbo.size[1], tmp) 80 | 81 | def upload_thumb(self): 82 | from kivy.graphics.texture import Texture 83 | w, h, pixels = self.thumb 84 | texture = Texture.create((w, h), 'rgb', 'ubyte') 85 | texture.blit_buffer(pixels, colorfmt='rgb') 86 | self.texture = texture 87 | self.texture_size = texture.size 88 | 89 | -------------------------------------------------------------------------------- /presemt/behaviours.py: -------------------------------------------------------------------------------- 1 | from kivy.factory import Factory 2 | from kivy.properties import BooleanProperty, ObjectProperty 3 | 4 | 5 | class ButtonBehavior(object): 6 | '''Button behavior. 7 | 8 | :Events: 9 | `on_press`: 10 | Fired when a touch is pressing the widget 11 | `on_release`: 12 | Fired when the first touch is up 13 | ''' 14 | 15 | is_hover = BooleanProperty(False) 16 | 17 | button_grab = BooleanProperty(False) 18 | 19 | button_touch = ObjectProperty(None, allownone=True) 20 | 21 | def __init__(self, **kwargs): 22 | super(ButtonBehavior, self).__init__(**kwargs) 23 | self.register_event_type('on_press') 24 | self.register_event_type('on_release') 25 | self.bind( 26 | on_touch_down=self._button_on_touch_down, 27 | on_touch_up=self._button_on_touch_up) 28 | 29 | def on_press(self, touch): 30 | pass 31 | 32 | def on_release(self, touch): 33 | pass 34 | 35 | def _button_on_touch_down(self, instance, touch): 36 | if not self.collide_point(*touch.pos): 37 | return 38 | touch.ungrab(self) 39 | touch.grab(self) 40 | self.is_hover = True 41 | self.button_touch = touch 42 | self.dispatch('on_press', touch) 43 | return self.button_grab 44 | 45 | def _button_on_touch_up(self, instance, touch): 46 | if touch.grab_current is not self: 47 | return 48 | touch.ungrab(self) 49 | self.is_hover = False 50 | self.dispatch('on_release', touch) 51 | self.button_touch = None 52 | return self.button_grab 53 | 54 | Factory.register('ButtonBehavior', cls=ButtonBehavior) 55 | 56 | 57 | class HoverBehavior(object): 58 | '''Hover behavior, but not used right now.' 59 | ''' 60 | 61 | is_hover = BooleanProperty(False) 62 | 63 | hover_grab = BooleanProperty(False) 64 | 65 | def __init__(self, **kwargs): 66 | self._hover_touch = None 67 | super(HoverBehavior, self).__init__(**kwargs) 68 | self.bind( 69 | on_touch_down=self._hover_on_touch_down, 70 | on_touch_move=self._hover_on_touch_move, 71 | on_touch_up=self._hover_on_touch_up) 72 | 73 | def _hover_on_touch_down(self, instance, touch): 74 | if self._hover_touch: 75 | return 76 | if not self.collide_point(*touch.pos): 77 | return 78 | touch.ungrab(self) 79 | touch.grab(self) 80 | self._hover_touch = touch 81 | self.is_hover = True 82 | return self.hover_grab 83 | 84 | def _hover_on_touch_move(self, instance, touch): 85 | if touch.grab_current is not self: 86 | return 87 | self.is_hover = self.collide_point(*touch.pos) 88 | return self.hover_grab 89 | 90 | def _hover_on_touch_up(self, instance, touch): 91 | if touch.grab_current is not self: 92 | return 93 | touch.ungrab(self) 94 | self.is_hover = False 95 | self._hover_touch = None 96 | return self.hover_grab 97 | 98 | Factory.register('HoverBehavior', cls=HoverBehavior) 99 | 100 | -------------------------------------------------------------------------------- /presemt/main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | PreseMT - A presentation software 3 | ================================ 4 | ''' 5 | 6 | import kivy 7 | kivy.require('1.0.6') 8 | 9 | from sys import argv 10 | from os.path import join, expanduser, dirname 11 | from shutil import rmtree 12 | from kivy.utils import QueryDict 13 | from kivy.app import App 14 | from kivy.uix.floatlayout import FloatLayout 15 | from kivy.clock import Clock 16 | 17 | # even if it's not used in this current files 18 | # behaviours are used into kv 19 | import behaviours 20 | import fbocapture 21 | 22 | 23 | class PresemtApp(App): 24 | def __init__(self): 25 | super(PresemtApp, self).__init__() 26 | self.screens = {} 27 | 28 | def build_config(self, config): 29 | config.add_section('paths') 30 | try: 31 | import android 32 | user_dir = '/sdcard' 33 | except ImportError: 34 | user_dir = expanduser('~') 35 | config.set('paths', 'workspace', join(user_dir, 'Documents', 'PreseMT')) 36 | 37 | def show(self, name): 38 | '''Create and show a screen widget 39 | ''' 40 | screens = self.screens 41 | modulename, clsname = name.split('.') 42 | if not name in screens: 43 | m = __import__('screens.%s' % modulename, fromlist=[modulename]) 44 | cls = getattr(m, clsname) 45 | screens[name] = cls(app=self) 46 | screen = screens[name] 47 | self.root.clear_widgets() 48 | self.root.add_widget(screen) 49 | return screen 50 | 51 | def unload(self, name): 52 | if name in self.screens: 53 | del self.screens[name] 54 | 55 | def show_start(self): 56 | self.show('project.SelectorScreen') 57 | 58 | def delete_project(self, filename): 59 | if not filename.startswith(self.config.get('paths', 'workspace')): 60 | return False 61 | directory = dirname(filename) 62 | try: 63 | rmtree(directory, True) 64 | except OSError: 65 | return False 66 | return True 67 | 68 | def create_empty_project(self): 69 | '''Create and start an empty project 70 | ''' 71 | self.unload('presentation.MainScreen') 72 | return self.show('presentation.MainScreen') 73 | 74 | def play_project(self, filename): 75 | project = self.create_empty_project() 76 | project.return_action = 'menu' 77 | project.filename = filename 78 | project.do_publish() 79 | 80 | def edit_project(self, filename=None): 81 | project = self.create_empty_project() 82 | if filename: 83 | project.filename = filename 84 | project.return_action = 'edit' 85 | project.do_edit() 86 | 87 | def build(self): 88 | self.root = FloatLayout() 89 | self.show('loading.LoadingScreen') 90 | Clock.schedule_once(self._async_load, .5) 91 | 92 | def _async_load(self, dt): 93 | # ... do loading here ^^ 94 | if len(argv) > 1: 95 | self.edit_project(argv[1]) 96 | else: 97 | self.show('project.SelectorScreen') 98 | 99 | if __name__ in ('__main__', '__android__'): 100 | PresemtApp().run() 101 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | *************************************************************************** 2 | PreseMT Copyright (C) 2011 Mathieu Virbel and Christopher Denter 3 | PreseMT may be used and distributed under the terms of the Q Public License 4 | as appearing in this file. 5 | *************************************************************************** 6 | 7 | The Q Public License Version 1.0 Copyright (C) 1999 Trolltech AS, Norway. 8 | 9 | Everyone is permitted to copy and distribute this license document. 10 | 11 | The intent of this license is to establish freedom to share and change the 12 | software regulated by this license under the open source model. 13 | 14 | This license applies to any software containing a notice placed by the 15 | copyright holder saying that it may be distributed under the terms of the Q 16 | Public License version 1.0. Such software is herein referred to as the 17 | Software. This license covers modification and distribution of the 18 | Software, use of third-party application programs based on the Software, 19 | and development of free software which uses the Software. 20 | 21 | Granted Rights 1. You are granted the non-exclusive rights set forth in 22 | this license provided you agree to and comply with any and all conditions 23 | in this license. Whole or partial distribution of the Software, or software 24 | items that link with the Software, in any form signifies acceptance of this 25 | license. 26 | 27 | 2. You may copy and distribute the Software in unmodified form provided 28 | that the entire package, including - but not restricted to - copyright, 29 | trademark notices and disclaimers, as released by the initial developer of 30 | the Software, is distributed. 31 | 32 | 3. You may make modifications to the Software and distribute your 33 | modifications, in a form that is separate from the Software, such as 34 | patches. The following restrictions apply to modifications: 35 | 36 | a. Modifications must not alter or remove any copyright notices in the 37 | Software. 38 | 39 | b. When modifications to the Software are released under this license, a 40 | non-exclusive royalty-free right is granted to the initial developer of the 41 | Software to distribute your modification in future versions of the Software 42 | provided such versions remain available under these terms in addition to 43 | any other license(s) of the initial developer. 44 | 45 | 4. You may distribute machine-executable forms of the Software or 46 | machine-executable forms of modified versions of the Software, provided 47 | that you meet these restrictions: 48 | 49 | a. You must include this license document in the distribution. 50 | 51 | b. You must ensure that all recipients of the machine-executable forms are 52 | also able to receive the complete machine-readable source code to the 53 | distributed Software, including all modifications, without any charge 54 | beyond the costs of data transfer, and place prominent notices in the 55 | distribution explaining this. 56 | 57 | c. You must ensure that all modifications included in the 58 | machine-executable forms are available under the terms of this license. 59 | 60 | 5. You may use the original or modified versions of the Software to 61 | compile, link and run application programs legally developed by you or by 62 | others. 63 | 64 | 6. You may develop application programs, reusable components and other 65 | software items that link with the original or modified versions of the 66 | Software. These items, when distributed, are subject to the following 67 | requirements: 68 | 69 | a. You must ensure that all recipients of machine-executable forms of these 70 | items are also able to receive and use the complete machine-readable source 71 | code to the items without any charge beyond the costs of data transfer. 72 | 73 | b. You must explicitly license all recipients of your items to use and 74 | re-distribute original and modified versions of the items in both 75 | machine-executable and source code forms. The recipients must be able to do 76 | so without any charges whatsoever, and they must be able to re-distribute 77 | to anyone they choose. 78 | 79 | c. If the items are not available to the general public, and the initial 80 | developer of the Software requests a copy of the items, then you must 81 | supply one. 82 | 83 | Limitations of Liability In no event shall the initial developers or 84 | copyright holders be liable for any damages whatsoever, including - but not 85 | restricted to - lost revenue or profits or other direct, indirect, special, 86 | incidental or consequential damages, even if they have been advised of the 87 | possibility of such damages, except to the extent invariable law, if any, 88 | provides otherwise. 89 | 90 | No Warranty The Software and this license document are provided AS IS with 91 | NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY 92 | AND FITNESS FOR A PARTICULAR PURPOSE. 93 | 94 | Choice of Law This license is governed by the Laws of France. Disputes 95 | shall be settled by Lille City Court. 96 | -------------------------------------------------------------------------------- /presemt/screens/project.py: -------------------------------------------------------------------------------- 1 | from . import Screen 2 | from os import listdir 3 | from os.path import join, exists 4 | from datetime import datetime 5 | 6 | from document import Document 7 | from kivy.lang import Builder 8 | from kivy.graphics.texture import Texture 9 | from kivy.properties import ObjectProperty, NumericProperty, StringProperty 10 | from kivy.animation import Animation 11 | from kivy.uix.floatlayout import FloatLayout 12 | 13 | class Modal(FloatLayout): 14 | alpha = NumericProperty(0.) 15 | app = ObjectProperty(None) 16 | def on_touch_down(self, touch): 17 | super(Modal, self).on_touch_down(touch) 18 | return True 19 | 20 | class ModalSelect(Modal): 21 | filename = StringProperty(None) 22 | 23 | class ModalConfirm(Modal): 24 | filename = StringProperty(None) 25 | 26 | class ModalHelp(Modal): 27 | pass 28 | 29 | class SelectorScreen(Screen): 30 | 31 | modalselect = ObjectProperty(None, allownone=True) 32 | 33 | modalconfirm = ObjectProperty(None, allownone=True) 34 | 35 | modalhelp = ObjectProperty(None, allownone=True) 36 | 37 | def __init__(self, **kwargs): 38 | super(SelectorScreen, self).__init__(**kwargs) 39 | 40 | def on_parent(self, instance, value): 41 | if value: 42 | self.refresh() 43 | 44 | def ask_load(self, filename): 45 | if self.modalselect: 46 | self.leave_load() 47 | else: 48 | self.modalselect = modalselect = ModalSelect( 49 | app=self, filename=filename) 50 | self.add_widget(modalselect) 51 | Animation(alpha=1, d=.5, t='out_cubic').start(modalselect) 52 | 53 | def leave_load(self): 54 | if not self.modalselect: 55 | return 56 | self.remove_widget(self.modalselect) 57 | self.modalselect = None 58 | 59 | def show_help(self): 60 | if self.modalhelp: 61 | return 62 | self.modalhelp = modalhelp = ModalHelp(app=self) 63 | self.add_widget(modalhelp) 64 | Animation(alpha=1, d=.5, t='out_cubic').start(modalhelp) 65 | 66 | def leave_help(self): 67 | if not self.modalhelp: 68 | return 69 | self.remove_widget(self.modalhelp) 70 | self.modalhelp = None 71 | 72 | 73 | def delete_project(self, filename, force=False): 74 | if force: 75 | self.app.delete_project(filename) 76 | self.refresh() 77 | self.leave_load() 78 | self.leave_delete() 79 | else: 80 | self.ask_delete(filename) 81 | 82 | def leave_delete(self): 83 | if not self.modalconfirm: 84 | return 85 | self.remove_widget(self.modalconfirm) 86 | self.modalconfirm = None 87 | 88 | def ask_delete(self, filename): 89 | if self.modalconfirm: 90 | self.leave_delete() 91 | else: 92 | self.modalconfirm = modalconfirm = ModalConfirm( 93 | app=self, filename=filename) 94 | self.add_widget(modalconfirm) 95 | Animation(alpha=1, d=.5, t='out_cubic').start(modalconfirm) 96 | 97 | def refresh(self): 98 | self.view.clear_widgets() 99 | self.search_documents() 100 | 101 | def search_documents(self): 102 | ws = self.app.config.get('paths', 'workspace') 103 | if not exists(ws): 104 | return 105 | docs = [] 106 | for item in listdir(ws): 107 | fn = join(ws, item, 'project.json') 108 | if not exists(fn): 109 | continue 110 | doc = Document() 111 | doc.load(fn) 112 | docs.append((doc, fn)) 113 | 114 | docs.sort(lambda a, b: cmp(a[0].infos.time_modification, b[0].infos.time_modification)) 115 | for doc, filename in docs: 116 | self.load_document(doc, filename) 117 | 118 | def load_document(self, doc, filename): 119 | view = self.view 120 | 121 | def thumb_texture(thumb): 122 | w, h, pixels = thumb 123 | texture = Texture.create((w, h), 'rgb', 'ubyte') 124 | texture.blit_buffer(pixels, colorfmt='rgb') 125 | return texture 126 | 127 | slides = list(doc.slides) 128 | title = '' 129 | texs = [thumb_texture(s.thumb) for s in slides[0:3]] 130 | texs.extend([None, None, None]) 131 | tex0, tex1, tex2 = texs[0:3] 132 | dt = datetime.fromtimestamp(doc.infos.time_modification) 133 | 134 | if tex0 is None: 135 | title = 'No preview available' 136 | 137 | item = Builder.template('SelectorItem', 138 | app=self, 139 | title=title, 140 | time=dt.strftime('%d/%m/%y %H:%M'), 141 | tex0=tex0, tex1=tex1, tex2=tex2, 142 | slide_count=len(slides), 143 | obj_count=len(list(doc.objects)), 144 | filename=filename) 145 | self.view.add_widget(item) 146 | 147 | def do_edit(self, filename): 148 | self.leave_load() 149 | print 'do_edit', filename 150 | self.app.edit_project(filename) 151 | 152 | def do_play(self, filename): 153 | self.leave_load() 154 | print 'do_play', filename 155 | self.app.play_project(filename) 156 | -------------------------------------------------------------------------------- /presemt/document.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Document 3 | ======== 4 | 5 | Available objects (1.0) 6 | ----------------------- 7 | 8 | Document: 9 | version - int 10 | time_creation - int 11 | time_modification - int 12 | size - tuple(int, int) 13 | 14 | 15 | Objects: 16 | 17 | [ common attributes of all objects ] 18 | pos - tuple(float, float) 19 | size - tuple(float, float) 20 | rotation - float 21 | scale - float 22 | 23 | Text: 24 | text - str 25 | bold - bool 26 | color - tuple(float, float, float, float) 27 | font_name - str 28 | font_size - float 29 | italic - bool 30 | 31 | Image: 32 | source - str 33 | 34 | Video: 35 | source - str 36 | 37 | Slides: 38 | Slide: 39 | pos - tuple(float, float) 40 | rotation - float 41 | scale - float 42 | ''' 43 | 44 | __all__ = ('Document', ) 45 | 46 | import json 47 | from time import time 48 | from kivy.utils import QueryDict 49 | 50 | class DocumentObject(QueryDict): 51 | __attrs__ = ('pos', 'size', 'rotation', 'scale', 'dtype') 52 | def __init__(self, **kwargs): 53 | super(DocumentObject, self).__init__(**kwargs) 54 | allowed_attrs = list(self.__class__.__attrs__) + \ 55 | list(super(self.__class__, self).__attrs__) 56 | if [x for x in self.keys() if x not in allowed_attrs]: 57 | raise Exception('You are using non allowed attributes for your ' 58 | 'object') 59 | 60 | class TextObject(DocumentObject): 61 | __attrs__ = ('text', 'bold', 'color', 'font_name', 'font_size', 'italic') 62 | 63 | 64 | class ImageObject(DocumentObject): 65 | __attrs__ = ('source', ) 66 | 67 | 68 | class VideoObject(DocumentObject): 69 | __attrs__ = ('source', ) 70 | 71 | 72 | class DocumentSlide(QueryDict): 73 | pass 74 | 75 | 76 | class Document(object): 77 | available_objects = {} 78 | 79 | def __init__(self, **kwargs): 80 | self.infos = QueryDict() 81 | self.infos.version = 1 82 | self.infos.time_creation = time() 83 | self.infos.time_modification = time() 84 | self.infos.root_size = kwargs.get('size', (100, 100)) 85 | self.infos.root_pos = kwargs.get('pos', (0, 0)) 86 | self.infos.root_scale = kwargs.get('scale', 0.) 87 | self.infos.root_rotation = kwargs.get('rotation', 0.) 88 | self._objects = [] 89 | self._slides = [] 90 | 91 | @staticmethod 92 | def register(name, cls): 93 | Document.available_objects[name] = cls 94 | 95 | @property 96 | def objects(self): 97 | return (QueryDict(x) for x in self._objects) 98 | 99 | @property 100 | def slides(self): 101 | return (QueryDict(x) for x in self._slides) 102 | 103 | def load(self, filename): 104 | with open(filename, 'r') as fd: 105 | j = json.loads(fd.read()) 106 | self.infos.update(j['document']) 107 | for slide in j['slides']: 108 | thumb = slide['thumb'] 109 | if thumb is not None: 110 | slide['thumb'] = self.decode_thumb(thumb) 111 | self._slides.append(DocumentSlide(slide)) 112 | for obj in j['objects']: 113 | # No dict comprehensions in 2.6 yet :'-( 114 | kwargs = {} 115 | for k, v in obj.iteritems(): 116 | kwargs[str(k)] = v 117 | inst = Document.available_objects[obj['dtype']](**kwargs) 118 | self._objects.append(inst) 119 | 120 | def save(self, filename): 121 | doc = QueryDict() 122 | doc.document = self.infos 123 | doc.document.time_modification = time() 124 | doc.objects = self._objects 125 | doc.slides = self._slides 126 | with open(filename, 'w') as fd: 127 | fd.write(json.dumps(doc)) 128 | 129 | def create_text(self, **attrs): 130 | text = TextObject(**attrs) 131 | text.dtype = 'text' 132 | self._objects.append(text) 133 | return text 134 | 135 | def create_image(self, **attrs): 136 | image = ImageObject(**attrs) 137 | image.dtype = 'image' 138 | self._objects.append(image) 139 | return image 140 | 141 | def create_video(self, **attrs): 142 | video = VideoObject(**attrs) 143 | video.dtype = 'video' 144 | self._objects.append(video) 145 | return video 146 | 147 | def encode_thumb(self, thumb): 148 | import pygame 149 | import tempfile 150 | import os 151 | import base64 152 | 153 | w, h, pixels = thumb 154 | # convert pixels to jpeg 155 | surface = pygame.image.fromstring(pixels, (w, h), 'RGB', True) 156 | fn = tempfile.mktemp('.jpg') 157 | pygame.image.save(surface, fn) 158 | # read jpeg 159 | with open(fn) as fd: 160 | data = fd.read() 161 | # delete file 162 | os.unlink(fn) 163 | # convert to base64 164 | data = base64.b64encode(data) 165 | return (w, h, 'data:image/jpeg;base64,' + data) 166 | 167 | def decode_thumb(self, thumb): 168 | header = 'data:image/jpeg;base64,' 169 | w, h, data = thumb 170 | if not data.startswith('data:image/jpeg;base64,'): 171 | return None 172 | data = data[len(header):] 173 | import base64 174 | import StringIO 175 | import pygame 176 | data = StringIO.StringIO(base64.b64decode(data)) 177 | surface = pygame.image.load(data, 'image.jpg') 178 | return (w, h, pygame.image.tostring(surface, 'RGB', True)) 179 | 180 | def add_slide(self, pos, rotation, scale, thumb): 181 | if thumb is not None: 182 | thumb = self.encode_thumb(thumb) 183 | slide = DocumentSlide() 184 | slide.pos = pos 185 | slide.rotation = rotation 186 | slide.scale = scale 187 | slide.thumb = thumb 188 | self._slides.append(slide) 189 | return slide 190 | 191 | def remove_slide(self, slide): 192 | self._slides.remove(slide) 193 | 194 | def clear_slides(self): 195 | self._slides = {} 196 | 197 | # register object that can be used in document 198 | Document.register('text', TextObject) 199 | Document.register('image', ImageObject) 200 | Document.register('video', VideoObject) 201 | 202 | if __name__ == '__main__': 203 | doc = Document() 204 | doc.create_text(text='Hello world', pos=(2, 3)) 205 | doc.save('output.json') 206 | doc = Document() 207 | doc.load('output.json') 208 | 209 | -------------------------------------------------------------------------------- /presemt/screens/presentation_plane.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Scatter plane with grid 3 | ''' 4 | 5 | from kivy.uix.scatter import ScatterPlane, Scatter 6 | from kivy.properties import NumericProperty, BooleanProperty 7 | from kivy.vector import Vector 8 | from kivy.clock import Clock 9 | from kivy.graphics import Line, Color 10 | from kivy.factory import Factory 11 | 12 | from presentation_objects import PlaneObject 13 | 14 | 15 | class MainPlane(ScatterPlane): 16 | 17 | grid_spacing = NumericProperty(50) 18 | 19 | grid_count = NumericProperty(1000) 20 | 21 | children_locked = BooleanProperty(False) 22 | 23 | def __init__(self, **kwargs): 24 | self._trigger_grid = Clock.create_trigger(self.fill_grid, -1) 25 | self._trigger_cull = Clock.create_trigger(self.cull_children, -1) 26 | super(MainPlane, self).__init__(**kwargs) 27 | self.register_event_type('on_scene_enter') 28 | self.register_event_type('on_scene_leave') 29 | self.all_children = [] 30 | self._trigger_grid() 31 | self._trigger_cull() 32 | 33 | def fill_grid(self, *largs): 34 | # FIXME grid disable cause every line is in a different VBO 35 | # That's cause lot of lags on many devices. Activate background 36 | # grid as soon as we will be able to do one call-one draw 37 | return 38 | self.canvas.clear() 39 | gs = self.grid_spacing 40 | gc = self.grid_count * gs 41 | count = 0 42 | with self.canvas: 43 | Color(.9, .9, .9, .2) 44 | for x in xrange(-gc, gc, gs): 45 | Line(points=(x, -gc, x, gc)) 46 | Line(points=(-gc, x, gc, x)) 47 | count += 2 48 | 49 | 50 | # 51 | # In order to maneuver more easily with a lot of widgets on the screen, we 52 | # want to be able to lock the children. Since we still want to handle the 53 | # touch events ourselves as if there were no children, we must overload the 54 | # on_touch_* handlers and do almost the same that our parent class does, 55 | # except that we only propagate the on_touch_* events to our children if 56 | # self.children_locked is False. 57 | # 58 | 59 | def on_touch_down(self, touch): 60 | if touch.device == 'wm_pen': 61 | return self.on_touch_down_pen(touch) 62 | return self.on_touch_down_touch(touch) 63 | 64 | def on_touch_move(self, touch): 65 | if touch.device == 'wm_pen': 66 | return self.on_touch_move_pen(touch) 67 | return self.on_touch_move_touch(touch) 68 | 69 | def on_touch_up(self, touch): 70 | if touch.device == 'wm_pen': 71 | return self.on_touch_up_pen(touch) 72 | return self.on_touch_up_touch(touch) 73 | 74 | def on_touch_down_touch(self, touch): 75 | x, y = touch.x, touch.y 76 | 77 | if not self.children_locked: 78 | # let the child widgets handle the event if they want 79 | touch.push() 80 | touch.apply_transform_2d(self.to_local) 81 | if super(Scatter, self).on_touch_down(touch): 82 | touch.pop() 83 | return True 84 | touch.pop() 85 | 86 | # grab the touch so we get all it later move events for sure 87 | touch.grab(self) 88 | 89 | if touch.is_double_tap: 90 | self.ctrl.selection_points = self.to_local(*touch.pos) 91 | else: 92 | self._touches.append(touch) 93 | self._last_touch_pos[touch] = touch.pos 94 | 95 | return True 96 | 97 | def on_touch_move_touch(self, touch): 98 | x, y = touch.x, touch.y 99 | if not self.children_locked: 100 | # let the child widgets handle the event if they want 101 | if not touch.grab_current == self: 102 | touch.push() 103 | touch.apply_transform_2d(self.to_local) 104 | if super(Scatter, self).on_touch_move(touch): 105 | touch.pop() 106 | return True 107 | touch.pop() 108 | 109 | # rotate/scale/translate 110 | if touch.grab_current is self: 111 | if touch.is_double_tap: 112 | self.ctrl.selection_points.extend( 113 | self.to_local(*touch.pos)) 114 | self.ctrl.update_select() 115 | elif touch in self._touches: 116 | self.transform_with_touch(touch) 117 | self._last_touch_pos[touch] = touch.pos 118 | 119 | return True 120 | 121 | def on_touch_up_touch(self, touch): 122 | x, y = touch.x, touch.y 123 | if not self.children_locked: 124 | # if the touch isnt on the widget we do nothing, just try children 125 | if not touch.grab_current == self: 126 | touch.push() 127 | touch.apply_transform_2d(self.to_local) 128 | if super(Scatter, self).on_touch_up(touch): 129 | touch.pop() 130 | return True 131 | touch.pop() 132 | 133 | # remove it from our saved touches 134 | if touch in self._touches and touch.grab_state: 135 | touch.ungrab(self) 136 | del self._last_touch_pos[touch] 137 | self._touches.remove(touch) 138 | 139 | return True 140 | 141 | def on_touch_down_pen(self, pen): 142 | if pen.is_double_tap: 143 | self.canvas.remove_group('lines') 144 | return True 145 | pen.push() 146 | pen.apply_transform_2d(self.to_local) 147 | with self.canvas: 148 | Color(1, 1, 1, group='lines') 149 | pen.ud.line = Line(points=list(pen.pos), group='lines') 150 | pen.pop() 151 | return True 152 | 153 | def on_touch_move_pen(self, pen): 154 | if 'line' not in pen.ud: 155 | return True 156 | line = pen.ud.line 157 | pen.push() 158 | pen.apply_transform_2d(self.to_local) 159 | line.points = line.points + list(pen.pos) 160 | pen.pop() 161 | return True 162 | 163 | def on_touch_up_pen(self, pen): 164 | return True 165 | 166 | # 167 | # Culling below 168 | # 169 | 170 | def is_visible(self, w): 171 | ''' 172 | Determine if planeobject w (a scatter itself) is visible in the current 173 | scatterplane viewport. Uses bounding circle check. 174 | ''' 175 | # Get minimal bounding circle around widget 176 | w_win_center = w.to_window(*w.pos) 177 | lwc = self.to_local(*w_win_center) 178 | # XXX Why does this not work instead of the previous two? 179 | #lwc = w.to_parent(*w.center) 180 | corner = w.to_parent(-w.width / 2., -w.height / 2.) 181 | r = Vector(*lwc).distance(Vector(*corner)) 182 | 183 | # Get minimal bounding circle around viewport 184 | # TODO If an optimization is required 185 | win = self.get_parent_window() 186 | if not win: 187 | return False 188 | cp = self.to_local(*win.center) 189 | #ww, wh = Window.size 190 | #topright = self.to_local(ww, wh) 191 | botleft = self.to_local(0, 0) 192 | wr = Vector(*cp).distance(botleft) 193 | 194 | dist = Vector(*cp).distance(Vector(lwc)) 195 | if dist - r <= wr: 196 | return True 197 | return False 198 | 199 | def transform_with_touch(self, touch): 200 | self._trigger_cull() 201 | super(MainPlane, self).transform_with_touch(touch) 202 | 203 | def on_scene_enter(self, child): 204 | # Reserved for further use; e.g. start video playback or whatever 205 | pass 206 | 207 | def on_scene_leave(self, child): 208 | # Reserved for further use; e.g. stop video playback or whatever 209 | pass 210 | 211 | def cull_children(self, *args, **kwargs): 212 | no_event = kwargs.get('no_event', False) 213 | # *args cause we use cull_children as a callback for animation's 214 | # on_progress 215 | old_children = self.children[:] 216 | self._really_clear_widgets() 217 | 218 | for child in reversed(self.all_children): 219 | if self.is_visible(child): 220 | self._really_add_widget(child) 221 | if not no_event and not child in old_children: 222 | self.dispatch('on_scene_enter', child) 223 | if no_event: 224 | return 225 | for child in old_children: 226 | if child not in self.children: 227 | self.dispatch('on_scene_leave', child) 228 | 229 | def add_widget(self, child): 230 | assert isinstance(child, PlaneObject) 231 | 232 | self.all_children.insert(0, child) 233 | self._really_add_widget(child, front=True) 234 | self._trigger_cull() 235 | 236 | def remove_widget(self, child): 237 | self.all_children.remove(child) 238 | self._really_remove_widget(child) 239 | self._trigger_cull() 240 | 241 | def clear_widgets(self): 242 | self.all_children = [] 243 | self._really_clear_widgets() 244 | 245 | def _really_add_widget(self, child, front=False): 246 | child.parent = self 247 | self.children.insert(0, child) 248 | self.canvas.add(child.canvas) 249 | 250 | def _really_remove_widget(self, child): 251 | self.children.remove(child) 252 | self.canvas.remove(child.canvas) 253 | 254 | def _really_clear_widgets(self): 255 | for child in self.children[:]: 256 | self._really_remove_widget(child) 257 | 258 | 259 | Factory.register('MainPlane', cls=MainPlane) 260 | -------------------------------------------------------------------------------- /presemt/screens/presentation.py: -------------------------------------------------------------------------------- 1 | from os.path import splitext 2 | from . import Screen 3 | from document import Document, TextObject, ImageObject, VideoObject 4 | from kivy.core.window import Window 5 | from kivy.clock import Clock 6 | from kivy.factory import Factory 7 | from kivy.uix.floatlayout import FloatLayout 8 | from kivy.properties import NumericProperty, ObjectProperty, \ 9 | BooleanProperty, ListProperty, AliasProperty, OptionProperty 10 | from kivy.animation import Animation 11 | from functools import partial 12 | from time import time 13 | from os.path import join 14 | from os import makedirs 15 | 16 | from config import SUPPORTED_VID, SUPPORTED_IMG 17 | import presentation_plane 18 | from presentation_panel import TextPanel, LocalFilePanel 19 | from presentation_objects import ImagePlaneObject, VideoPlaneObject, \ 20 | TextPlaneObject 21 | from presentation_slides import Slide 22 | 23 | def point_inside_polygon(x,y,poly): 24 | '''Taken from http://www.ariel.com.au/a/python-point-int-poly.html''' 25 | 26 | n = len(poly) 27 | inside = False 28 | 29 | p1x = poly[0] 30 | p1y = poly[1] 31 | for i in xrange(0, n+2, 2): 32 | p2x = poly[i % n] 33 | p2y = poly[(i+1) % n] 34 | if y > min(p1y,p2y): 35 | if y <= max(p1y,p2y): 36 | if x <= max(p1x,p2x): 37 | if p1y != p2y: 38 | xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x 39 | if p1x == p2x or x <= xinters: 40 | inside = not inside 41 | p1x,p1y = p2x,p2y 42 | 43 | return inside 44 | 45 | # 46 | # Main screen, act as a controler for everybody 47 | # 48 | 49 | class MainScreen(Screen): 50 | 51 | modalquit = ObjectProperty(None, allownone=True) 52 | 53 | def _get_filename(self): 54 | return self._filename 55 | def _set_filename(self, filename): 56 | self._filename = filename 57 | return True 58 | filename = AliasProperty(_get_filename, _set_filename) 59 | 60 | return_action = OptionProperty('edit', options=('edit', 'menu')) 61 | 62 | is_edit = BooleanProperty(False) 63 | 64 | is_dirty = BooleanProperty(False) 65 | 66 | plane = ObjectProperty(None) 67 | 68 | config = ObjectProperty(None) 69 | 70 | capture = ObjectProperty(None) 71 | 72 | do_selection = BooleanProperty(False) 73 | 74 | selection_points = ListProperty([0, 0]) 75 | 76 | tb_objects = ObjectProperty(None) 77 | 78 | tb_slides = ObjectProperty(None) 79 | 80 | btn_savefile = ObjectProperty(None) 81 | 82 | def __init__(self, **kwargs): 83 | self._filename = None 84 | self._initial_load = True 85 | self._panel = None 86 | self._panel_text = None 87 | self._panel_localfile = None 88 | self._plane_animation = None 89 | self.trigger_slides = Clock.create_trigger( 90 | self.update_slides_capture, 1) 91 | super(MainScreen, self).__init__(**kwargs) 92 | 93 | def on_parent(self, instance, value): 94 | try: 95 | # don't do keyboard shorcut on android platform 96 | import android 97 | return 98 | except ImportError: 99 | pass 100 | if value is not None: 101 | Window.bind(on_keyboard=self.on_window_keyboard) 102 | else: 103 | Window.unbind(on_keyboard=self.on_window_keyboard) 104 | 105 | def on_window_keyboard(self, window, code, *largs): 106 | # edit mode 107 | if self.is_edit: 108 | if code == 27: 109 | if self._panel: 110 | self.toggle_panel() 111 | else: 112 | self.ask_quit() 113 | return True 114 | 115 | # publish mode 116 | else: 117 | # keyboard shortcut for publish mode 118 | slide = self.get_selected_slide() 119 | if not slide: 120 | return 121 | # left pad, previous page 122 | if code in (276, 280): 123 | slide = self.get_slide_by_index(slide.index - 1) 124 | self.select_slide(slide) 125 | # right pad, next page 126 | elif code in (275, 281): 127 | slide = self.get_slide_by_index(slide.index + 1) 128 | self.select_slide(slide) 129 | # space bar, focus current page 130 | elif code == 32: 131 | self.select_slide(slide) 132 | # back to edit mode 133 | elif code == 101: 134 | self.do_edit() 135 | # escape 136 | elif code == 27: 137 | self.ask_quit() 138 | else: 139 | return 140 | return True 141 | 142 | def on_btn_savefile(self, instance, value): 143 | value.visible = self.is_dirty 144 | 145 | def on_is_dirty(self, instance, value): 146 | self.btn_savefile.visible = value 147 | 148 | def go_return_action(self): 149 | if self.return_action == 'menu': 150 | self.ask_quit(force=True) 151 | else: 152 | self.do_edit() 153 | 154 | def leave_quit(self): 155 | self.remove_widget(self.modalquit) 156 | self.modalquit = None 157 | 158 | def ask_quit(self, force=False): 159 | if force is True or not self.is_dirty: 160 | self.app.show_start() 161 | return 162 | if self.is_dirty: 163 | if self.modalquit: 164 | self.leave_quit() 165 | else: 166 | self.modalquit = modalquit = ModalQuit(app=self) 167 | self.add_widget(modalquit) 168 | Animation(alpha=1, d=.5, t='out_cubic').start(modalquit) 169 | 170 | def set_dirty(self): 171 | if not self.is_edit: 172 | return 173 | self.is_dirty = True 174 | 175 | def _create_object(self, cls, touch, **kwargs): 176 | self.set_dirty() 177 | kwargs.setdefault('rotation', -self.plane.rotation) 178 | kwargs.setdefault('scale', 1. / self.plane.scale) 179 | obj = cls(touch_follow=touch, ctrl=self, **kwargs) 180 | if 'size' in kwargs: 181 | obj.size = kwargs.get('size') 182 | if 'scale' in kwargs: 183 | obj.rotation = kwargs.get('scale') 184 | if 'rotation' in kwargs: 185 | obj.rotation = kwargs.get('rotation') 186 | if 'pos' in kwargs: 187 | obj.pos = kwargs.get('pos') 188 | else: 189 | obj.pos = self.plane.to_local(*self.center) 190 | self.plane.add_widget(obj) 191 | 192 | def update_select(self): 193 | s = self.selection_points 194 | for child in self.plane.children: 195 | child.selected = point_inside_polygon( 196 | child.x, child.y, s) 197 | 198 | def selection_align(self): 199 | childs = [x for x in self.plane.children if x.selected] 200 | if not childs: 201 | return 202 | # do align on x 203 | left = min([x.x for x in childs]) 204 | right = max([x.right for x in childs]) 205 | middle = left + (right - left) / 2. 206 | for child in childs: 207 | child.center_x = middle 208 | 209 | def cancel_selection(self): 210 | self.do_selection = False 211 | self.selection_points = [0, 0] 212 | for child in self.plane.children: 213 | child.selected = False 214 | 215 | def create_text(self, touch=None, **kwargs): 216 | self._create_object(TextPlaneObject, touch, **kwargs) 217 | 218 | def from_localfile(self, touch, **kwargs): 219 | kwargs.setdefault('do_adjust', True) 220 | source = kwargs['source'] 221 | ext = splitext(source)[-1][1:] 222 | if ext in SUPPORTED_IMG: 223 | self.create_image(touch, **kwargs) 224 | elif ext in SUPPORTED_VID: 225 | self.create_video(touch, **kwargs) 226 | 227 | def create_image(self, touch=None, **kwargs): 228 | self._create_object(ImagePlaneObject, touch, **kwargs) 229 | 230 | def create_video(self, touch=None, **kwargs): 231 | self._create_object(VideoPlaneObject, touch, **kwargs) 232 | 233 | def get_text_panel(self): 234 | if not self._panel_text: 235 | self._panel_text = TextPanel(ctrl=self) 236 | return self._panel_text 237 | 238 | def get_localfile_panel(self): 239 | if not self._panel_localfile: 240 | self._panel_localfile = LocalFilePanel(ctrl=self) 241 | return self._panel_localfile 242 | 243 | def toggle_panel(self, name=None): 244 | panel = None 245 | if name: 246 | panel = getattr(self, 'get_%s_panel' % name)() 247 | if self._panel: 248 | same = self._panel is panel 249 | if same: 250 | panel = None 251 | anim = Animation(width=0, d=.3, t='out_cubic') 252 | anim.bind(on_complete=partial(self._anim_show_panel, panel)) 253 | anim.start(self.config_container) 254 | return 255 | if panel: 256 | self._anim_show_panel(panel) 257 | 258 | def _anim_show_panel(self, panel, *largs): 259 | if self._panel: 260 | self._panel.dispatch('on_close') 261 | getattr(self, self._panel.status_btn).state = 'normal' 262 | self.config.remove_widget(self._panel) 263 | self._panel = panel 264 | if self._panel: 265 | self.config.add_widget(self._panel) 266 | self._panel.dispatch('on_open') 267 | getattr(self, self._panel.status_btn).state = 'down' 268 | anim = Animation(width=350, d=.3, t='out_cubic') 269 | anim.start(self.config_container) 270 | 271 | # used for kv button 272 | def toggle_text_panel(self): 273 | self.toggle_panel('text') 274 | 275 | def toggle_localfile_panel(self): 276 | self.toggle_panel('localfile') 277 | 278 | def toggle_lock(self): 279 | self.plane.children_locked = not self.plane.children_locked 280 | 281 | # 282 | # Save/Load 283 | # 284 | 285 | def do_save(self): 286 | doc = Document(size=self.size, pos=self.plane.pos, 287 | scale=self.plane.scale, rotation=self.plane.rotation) 288 | for obj in reversed(self.plane.all_children): 289 | attrs = [ ('pos', obj.pos), ('size', obj.size), 290 | ('rotation', obj.rotation), ('scale', obj.scale)] 291 | if isinstance(obj, TextPlaneObject): 292 | attrs += [(attr, getattr(obj, attr)) for attr in TextObject.__attrs__] 293 | doc.create_text(**dict(attrs)) 294 | elif isinstance(obj, ImagePlaneObject): 295 | attrs += [(attr, getattr(obj, attr)) for attr in ImageObject.__attrs__] 296 | doc.create_image(**dict(attrs)) 297 | elif isinstance(obj, VideoPlaneObject): 298 | attrs += [(attr, getattr(obj, attr)) for attr in VideoObject.__attrs__] 299 | doc.create_video(**dict(attrs)) 300 | 301 | for obj in reversed(self.tb_slides.children): 302 | obj.download_thumb() 303 | doc.add_slide(obj.slide_pos, obj.slide_rotation, 304 | obj.slide_scale, obj.thumb) 305 | 306 | ws = self.app.config.get('paths', 'workspace') 307 | if not self.filename: 308 | project_dir = join(ws, 'project_%d' % time()) 309 | makedirs(project_dir) 310 | self._filename = join(project_dir, 'project.json') 311 | doc.save(self.filename) 312 | self.is_dirty = False 313 | 314 | def on_filename(self, instance, filename): 315 | start = time() 316 | doc = Document() 317 | doc.load(filename) 318 | self.plane.size = doc.infos.root_size 319 | self.plane.scale = doc.infos.root_scale 320 | self.plane.rotation = doc.infos.root_rotation 321 | self.plane.pos = doc.infos.root_pos 322 | for obj in doc.objects: 323 | attrs = [ ('pos', obj.pos), ('size', obj.size), 324 | ('rotation', obj.rotation), ('scale', obj.scale)] 325 | if obj.dtype == 'text': 326 | attrs += [(attr, obj[attr]) for attr in TextObject.__attrs__] 327 | self.create_text(**dict(attrs)) 328 | elif obj.dtype == 'image': 329 | attrs += [(attr, obj[attr]) for attr in ImageObject.__attrs__] 330 | self.create_image(**dict(attrs)) 331 | elif obj.dtype == 'video': 332 | attrs += [(attr, obj[attr]) for attr in VideoObject.__attrs__] 333 | self.create_video(**dict(attrs)) 334 | for obj in doc.slides: 335 | self.create_slide(pos=obj.pos, rotation=obj.rotation, 336 | scale=obj.scale, thumb=obj.thumb) 337 | self.is_dirty = False 338 | print '=== Loading time: %3.4s' % (time() - start) 339 | 340 | # 341 | # Presentation 342 | # 343 | def _do_initial_load(self): 344 | for widget in self.container_edit: 345 | widget.old_parent = widget.parent 346 | widget.parent.remove_widget(widget) 347 | for widget in self.container_publish: 348 | widget.old_parent = widget.parent 349 | widget.parent.remove_widget(widget) 350 | self._initial_load = False 351 | 352 | def do_publish(self): 353 | if self._initial_load: 354 | self._do_initial_load() 355 | self.plane.children_locked = True 356 | for widget in self.container_edit: 357 | if not widget.parent: 358 | continue 359 | widget.old_parent = widget.parent 360 | widget.parent.remove_widget(widget) 361 | for widget in self.container_publish: 362 | widget.old_parent.add_widget(widget) 363 | self.is_edit = False 364 | 365 | def do_edit(self): 366 | if self._initial_load: 367 | self._do_initial_load() 368 | self.plane.children_locked = False 369 | for widget in self.container_publish: 370 | if not widget.parent: 371 | continue 372 | widget.old_parent = widget.parent 373 | widget.parent.remove_widget(widget) 374 | for widget in self.container_edit: 375 | widget.old_parent.add_widget(widget) 376 | self.is_edit = True 377 | 378 | # 379 | # Objects 380 | # 381 | 382 | def remove_object(self, obj): 383 | self.set_dirty() 384 | self.plane.remove_widget(obj) 385 | 386 | def configure_object(self, obj): 387 | # FIXME TODO 388 | pass 389 | 390 | # 391 | # Slides 392 | # 393 | 394 | def reset_animation(self): 395 | if not self._plane_animation: 396 | return 397 | self._plane_animation.stop(self.plane) 398 | self._plane_animation = None 399 | 400 | def create_slide(self, pos=None, rotation=None, scale=None, thumb=None): 401 | self.set_dirty() 402 | plane = self.plane 403 | pos = pos or plane.pos 404 | scale = scale or plane.scale 405 | rotation = rotation or plane.rotation 406 | 407 | slide = Slide(ctrl=self, 408 | slide_pos=pos, 409 | slide_rotation=rotation, 410 | slide_scale=scale, 411 | thumb=thumb) 412 | self.tb_slides.add_widget(slide) 413 | self.update_slide_index() 414 | 415 | def remove_slide(self, slide): 416 | self.set_dirty() 417 | self.unselect_slides() 418 | self.tb_slides.remove_widget(slide) 419 | self.update_slide_index() 420 | 421 | def select_slide(self, slide): 422 | k = {'d': .5, 't': 'out_cubic'} 423 | 424 | # highlight slide 425 | self.unselect() 426 | slide.selected = True 427 | 428 | # rotation must be fixed by hand 429 | slide_rotation = slide.slide_rotation 430 | s = abs(slide_rotation - self.plane.rotation) 431 | if s > 180: 432 | if slide_rotation > self.plane.rotation: 433 | slide_rotation -= 360 434 | else: 435 | slide_rotation += 360 436 | 437 | # move to the correct position in the place 438 | self._plane_animation = Animation(pos=slide.slide_pos, 439 | rotation=slide_rotation, 440 | scale=slide.slide_scale, **k) 441 | self._plane_animation.bind(on_progress=self.plane.cull_children) 442 | self._plane_animation.start(self.plane) 443 | 444 | def unselect(self): 445 | self.selection_points = [0, 0] 446 | self.reset_animation() 447 | self.unselect_slides() 448 | 449 | def get_selected_slide(self): 450 | for child in self.tb_slides.children: 451 | if child.selected: 452 | return child 453 | if len(self.tb_slides.children): 454 | return self.tb_slides.children[0] 455 | 456 | def get_slide_by_index(self, index): 457 | index = index % len(self.tb_slides.children) 458 | for x in self.tb_slides.children: 459 | if x.index == index: 460 | return x 461 | 462 | def go_next_slide(self): 463 | slide = self.get_selected_slide() 464 | if not slide: 465 | return 466 | self.select_slide(self.get_slide_by_index(slide.index + 1)) 467 | 468 | def go_previous_slide(self): 469 | slide = self.get_selected_slide() 470 | if not slide: 471 | return 472 | self.select_slide(self.get_slide_by_index(slide.index - 1)) 473 | 474 | def unselect_slides(self): 475 | for child in self.tb_slides.children: 476 | child.selected = False 477 | 478 | def update_slide_index(self): 479 | for idx, slide in enumerate(reversed(self.tb_slides.children)): 480 | slide.index = idx 481 | 482 | def update_slides_capture(self, *largs): 483 | if not self.is_edit: 484 | return 485 | pos = self.plane.pos 486 | scale = self.plane.scale 487 | rotation = self.plane.rotation 488 | for slide in self.tb_slides.children: 489 | self.plane.scale = slide.slide_scale 490 | self.plane.rotation = slide.slide_rotation 491 | self.plane.pos = slide.slide_pos 492 | self.plane.cull_children(no_event=True) 493 | slide.update_capture() 494 | self.plane.scale = scale 495 | self.plane.rotation = rotation 496 | self.plane.pos = pos 497 | self.plane.cull_children(no_event=True) 498 | 499 | 500 | class ModalQuit(FloatLayout): 501 | 502 | alpha = NumericProperty(0.) 503 | 504 | app = ObjectProperty(None) 505 | 506 | def on_touch_down(self, touch): 507 | super(ModalQuit, self).on_touch_down(touch) 508 | return True 509 | 510 | Factory.register('ModalQuit', cls=ModalQuit) 511 | 512 | 513 | -------------------------------------------------------------------------------- /presemt/presemt.kv: -------------------------------------------------------------------------------- 1 | #:kivy 1.0 2 | #:import fc functools 3 | 4 | # Color scheme 5 | #:set bg_source 'data/images/background.jpg' 6 | # dad6ca (light), 5f6062 (dark) 7 | ##:set bg_color [x / 255. for x in (0x5f, 0x60, 0x62, 0xff)] 8 | ##:set tb_color [x / 255. for x in (0xda, 0xd6, 0xca, 0xff)] 9 | ##:set icon_color [x / 255. for x in (0x5f, 0x60, 0x62, 0x66)] 10 | ##:set icon_color_selected [x / 255. for x in (0x5f, 0x60, 0x62, 0xff)] 11 | 12 | #:set bg_color [x / 255. for x in (0x41, 0x41, 0x41, 0xff)] 13 | #:set icon_color [x / 255. for x in (0xbb, 0xbb, 0xbb, 0xaa)] 14 | #:set icon_color_selected [x / 255. for x in (0xee, 0xee, 0xee, 0xff)] 15 | #:set tb_color [x / 255. for x in (0x51, 0x51, 0x51, 0xff)] 16 | #:set title_color [x / 255. for x in (0xee, 0xee, 0xee, 0xff)] 17 | 18 | : 19 | canvas: 20 | Color: 21 | rgba: 1, 1, 1, 1 22 | Rectangle: 23 | source: bg_source 24 | size: self.size 25 | 26 | : 27 | AnchorLayout: 28 | Label: 29 | text: 'PreseMT is loading...' 30 | color: title_color 31 | 32 | : 33 | canvas: 34 | Color: 35 | rgba: 0, 0, 0, self.alpha * 0.9 36 | Rectangle: 37 | pos: self.pos 38 | size: self.size 39 | 40 | : 41 | canvas: 42 | Color: 43 | rgba: 0, 0, 0, self.alpha * 0.9 44 | Rectangle: 45 | pos: self.pos 46 | size: self.size 47 | 48 | BoxLayout: 49 | spacing: 20 50 | size_hint: None, None 51 | center_y: root.top - root.center_y * root.alpha 52 | center_x: root.center_x 53 | 54 | ToolbarButton: 55 | image: 'left' 56 | callback: root.app.leave_delete 57 | no_toggle: True 58 | size: 56, 56 59 | 60 | ToolbarButton: 61 | image: 'thumbs-up' 62 | callback: fc.partial(root.app.delete_project, root.filename, True) 63 | no_toggle: True 64 | size: 56, 56 65 | 66 | : 67 | BoxLayout: 68 | spacing: 20 69 | size_hint: None, None 70 | center_y: root.top - root.center_y * root.alpha 71 | center_x: root.center_x 72 | 73 | ToolbarButton: 74 | image: 'left' 75 | callback: root.app.leave_load 76 | no_toggle: True 77 | size: 56, 56 78 | 79 | ToolbarButton: 80 | image: 'trash' 81 | callback: fc.partial(root.app.delete_project, root.filename) 82 | no_toggle: True 83 | size: 56, 56 84 | 85 | ToolbarButton: 86 | image: 'edit' 87 | callback: fc.partial(root.app.do_edit, root.filename) 88 | no_toggle: True 89 | size: 56, 56 90 | 91 | ToolbarButton: 92 | image: 'eye' 93 | callback: fc.partial(root.app.do_play, root.filename) 94 | no_toggle: True 95 | size: 56, 56 96 | 97 | [HelpImage@Image]: 98 | source: ctx.source 99 | size_hint_x: None 100 | width: 800 101 | canvas.before: 102 | Color: 103 | rgba: 1, 1, 1, .5 104 | Rectangle: 105 | size: self.norm_image_size[0] + 4, self.norm_image_size[1] + 4 106 | pos: self.center_x - self.norm_image_size[0] / 2. - 2, self.center_y - self.norm_image_size[1] / 2. - 2 107 | 108 | : 109 | BoxLayout: 110 | orientation: 'vertical' 111 | 112 | Widget: 113 | size_hint_y: None 114 | height: 56 115 | 116 | ScrollView: 117 | do_scroll_y: False 118 | BoxLayout: 119 | orientation: 'horizontal' 120 | size_hint_x: None 121 | padding: 10 122 | spacing: 10 123 | HelpImage: 124 | source: 'help/selector_0.jpg' 125 | HelpImage: 126 | source: 'help/selector_1.jpg' 127 | HelpImage: 128 | source: 'help/player_0.jpg' 129 | HelpImage: 130 | source: 'help/main_0.jpg' 131 | HelpImage: 132 | source: 'help/main_1.jpg' 133 | HelpImage: 134 | source: 'help/main_2.jpg' 135 | 136 | ToolbarButton: 137 | image: 'left' 138 | callback: root.app.leave_help 139 | no_toggle: True 140 | size: 56, 56 141 | 142 | 143 | 144 | [SelectorItem@ButtonBehavior+FloatLayout]: 145 | size_hint_x: None 146 | width: 256 147 | canvas: 148 | Color: 149 | rgb: 1, 1, 1 150 | Rectangle: 151 | source: 'data/selector.png' 152 | size: self.size 153 | pos: self.pos 154 | Color: 155 | rgba: 1, 1, 1, .3 156 | Rectangle: 157 | texture: ctx.tex2 158 | size: ctx['tex2'].size if ctx['tex2'] else (0, 0) 159 | pos: (self.center_x - ctx['tex2'].size[0] / 2. + 20, self.center_y - ctx['tex2'].size[1] / 2.) if ctx['tex2'] else (0, 0) 160 | Color: 161 | rgba: 1, 1, 1, .7 162 | Rectangle: 163 | texture: ctx.tex1 164 | size: ctx['tex1'].size if ctx['tex1'] else (0, 0) 165 | pos: (self.center_x - ctx['tex1'].size[0] / 2. - 20, self.center_y - ctx['tex1'].size[1] / 2. + 40) if ctx['tex1'] else (0, 0) 166 | Color: 167 | rgb: 1, 1, 1 168 | Rectangle: 169 | texture: ctx.tex0 170 | size: ctx['tex0'].size if ctx['tex0'] else (0, 0) 171 | pos: (self.center_x - ctx['tex0'].size[0] / 2., self.center_y - ctx['tex0'].size[1] / 2. + 20) if ctx['tex0'] else (0, 0) 172 | button_grab: True 173 | on_press: ctx.app.ask_load(ctx.filename) 174 | 175 | BoxLayout: 176 | pos: root.pos 177 | orientation: 'vertical' 178 | padding: 8 179 | Label: 180 | color: bg_color 181 | text: ctx.title 182 | BoxLayout: 183 | size_hint_y: None 184 | height: 50 185 | BoxLayout: 186 | orientation: 'vertical' 187 | padding: 10 188 | Label: 189 | size_hint_y: None 190 | color: bg_color 191 | height: self.texture_size[1] 192 | text: ctx.time 193 | Label: 194 | size_hint_y: None 195 | height: self.texture_size[1] 196 | font_size: 10 197 | color: bg_color[:3] + [.6] 198 | text: '%d slides, %d objects' % (ctx.slide_count, ctx.obj_count) 199 | 200 | : 201 | view: view 202 | AnchorLayout: 203 | BoxLayout: 204 | orientation: 'vertical' 205 | size_hint_y: None 206 | 207 | BoxLayout: 208 | padding: 20 209 | spacing: 20 210 | id: firstbox 211 | 212 | Label: 213 | text: 'PreseMT' 214 | text_size: self.width - 20, self.height 215 | font_size: 28 216 | color: title_color 217 | size_hint_y: None 218 | height: 56 219 | 220 | ToolbarButton: 221 | image: 'info' 222 | callback: root.show_help 223 | no_toggle: True 224 | 225 | ToolbarButton: 226 | image: 'refresh' 227 | callback: root.refresh 228 | no_toggle: True 229 | 230 | ToolbarButton: 231 | image: 'add' 232 | callback: root.app.edit_project 233 | no_toggle: True 234 | 235 | 236 | ScrollView: 237 | size_hint_y: None 238 | height: 276 239 | id: scrollview 240 | StackLayout: 241 | spacing: 10 242 | padding: 10 243 | orientation: 'tb-lr' 244 | id: view 245 | size_hint_x: None 246 | 247 | Widget: 248 | size_hint: None, None 249 | size: firstbox.size 250 | 251 | AnchorLayout: 252 | padding: 10 253 | anchor_x: 'right' 254 | pos_hint: {'right': 1} 255 | size_hint_y: None 256 | height: label.height + 10 257 | Label: 258 | id: label 259 | size_hint: None, None 260 | size: self.texture_size 261 | color: (1, 1, 1, .2) 262 | halign: 'right' 263 | text: 'Made with Kivy : http://kivy.org/\nBy Mathieu Virbel and Christopher Denter' 264 | 265 | : 266 | size_hint: None, None 267 | canvas: 268 | Color: 269 | rgba: ((1, 1, 0, .4) if self.selected else (0, 1, 0, .4)) if root.ctrl.is_edit else (0, 0, 0, 0) 270 | Rectangle: 271 | size: self.size 272 | pos: -self.width / 2., -self.height / 2. 273 | 274 | : 275 | size: label.size 276 | Label: 277 | id: label 278 | pos: -self.width / 2., -self.height / 2. 279 | size: self.texture_size 280 | text: root.text 281 | font_size: root.font_size 282 | 283 | : 284 | size: image.size 285 | Image: 286 | id: image 287 | pos: -self.width / 2., -self.height / 2. 288 | size: self.texture_size 289 | source: root.source 290 | 291 | : 292 | size: video.size 293 | Video: 294 | id: video 295 | pos: -self.width / 2., -self.height / 2. 296 | size: self.texture_size 297 | source: root.source 298 | play: True 299 | 300 | # 301 | # Panels 302 | # 303 | [PanelTitle@Label] 304 | font_size: 24 305 | text: ctx.title 306 | size_hint_y: None 307 | height: self.texture_size[1] 308 | text_size: (self.width, None) 309 | color: title_color 310 | 311 | : 312 | x: 56 313 | canvas: 314 | Color: 315 | rgba: bg_color 316 | #(.16, .18, .19, .9) 317 | BorderImage: 318 | border: 0, 16, 0, 0 319 | source: 'data/toolbar_left.png' 320 | pos: self.pos 321 | size: self.size 322 | 323 | [ImageButtonPanel@ButtonBehavior+Image]: 324 | source: ctx.source 325 | size: self.texture_size 326 | size_hint_x: None 327 | button_grab: True 328 | on_press: ctx.callback() 329 | 330 | : 331 | width: self.texture_size[0] 332 | button_grab: True 333 | size_hint_x: None 334 | 335 | : 336 | size_hint_y: None 337 | size: [max(40, x + 20) for x in label.texture_size] 338 | padding: 10 339 | 340 | canvas.before: 341 | Color: 342 | rgba: .1, .1, .1, .5 343 | Rectangle: 344 | pos: self.pos 345 | size: self.size 346 | 347 | Label: 348 | id: label 349 | text: root.text 350 | text_size: (self.width - 20, None) 351 | 352 | ImageButton: 353 | source: 'data/text-del.png' 354 | on_press: root.panel.stack.remove_widget(root) 355 | 356 | 357 | : 358 | width: 300 359 | size_hint_x: None 360 | textinput: textinput 361 | stack: stack 362 | on_close: textinput.focus = False 363 | 364 | BoxLayout: 365 | padding: 10 366 | spacing: 10 367 | pos: root.pos 368 | orientation: 'vertical' 369 | 370 | PanelTitle: 371 | title: 'Add a text' 372 | 373 | BoxLayout: 374 | size_hint_y: None 375 | height: max(32, textinput.height) 376 | spacing: 20 377 | 378 | TextInput: 379 | id: textinput 380 | 381 | ImageButtonPanel: 382 | source: 'data/text-add.png' 383 | callback: root.add_text 384 | 385 | ScrollView: 386 | scroll_timeout: 100 387 | StackLayout: 388 | id: stack 389 | size_hint_y: None 390 | 391 | 392 | 393 | : 394 | width: 340 395 | size_hint_x: None 396 | 397 | BoxLayout: 398 | orientation: 'vertical' 399 | pos: root.pos 400 | size: root.size 401 | padding: 10 402 | spacing: 10 403 | 404 | PanelTitle: 405 | title: 'Add a media' 406 | 407 | FileChooserIconView: 408 | path: root.path 409 | filters: root.imgtypes if img.only else root.vidtypes if vid.only else root.suptypes 410 | on_submit: root.ctrl.from_localfile(args[2], source=args[1][0]) 411 | 412 | BoxLayout: 413 | orientation: 'horizontal' 414 | size_hint_y: None 415 | height: 40 416 | spacing: 10 417 | ToggleButton: 418 | id: img 419 | only: self.state == 'down' 420 | text: 'Images' 421 | group: 'filetype' 422 | ToggleButton: 423 | id: vid 424 | only: self.state == 'down' 425 | text: 'Videos' 426 | group: 'filetype' 427 | 428 | 429 | [ToolbarButton@ToggleButton]: 430 | source: 'data/%s.png' % ctx.image 431 | size: ctx.size if 'size' in ctx else image.texture_size 432 | size_hint: ctx.size_hint if 'size_hint' in ctx else (None, None) 433 | on_release: ctx.callback(); self._do_press() if ctx.no_toggle else None 434 | #button_grab: True 435 | group: ctx.group if 'group' in ctx else None 436 | visible: True 437 | no_toggle: ctx.no_toggle if 'no_toggle' in ctx else False 438 | canvas: 439 | Clear 440 | Image: 441 | id: image 442 | source: root.source if root.visible else 'data/transparent.png' 443 | color: ctx.color if 'color' in ctx else (1, 1, 1, 1) 444 | color: (ctx.color if 'color' in ctx else icon_color) if root.state == 'normal' else icon_color_selected 445 | size_hint: (None, None) 446 | size: [x * 0.8 for x in root.size] 447 | x: root.center_x - 0.4 * root.width 448 | y: root.center_y - 0.4 * root.height 449 | 450 | 451 | : 452 | button_grab: True 453 | size_hint_y: None 454 | height: 54 455 | canvas.after: 456 | Color: 457 | rgba: [0.5411, 0.6862, 0.2352, .1] if root.selected else (0, 0, 0, 0) 458 | Rectangle: 459 | size: self.norm_image_size 460 | pos: self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2. 461 | 462 | #height: self.norm_image_size[1] + label.height * 2 463 | 464 | Label: 465 | id: label 466 | text: str(root.index + 1) 467 | x: root.x 468 | top: root.top 469 | font_size: 8 470 | size: (root.width, self.texture_size[1] + 5) 471 | canvas: 472 | Color: 473 | rgba: [0.5411, 0.6862, 0.2352, 1] if root.selected else (0, 0, 0, 1) 474 | Rectangle: 475 | pos: self.pos 476 | size: self.size 477 | 478 | : 479 | canvas: 480 | Color: 481 | rgb: 1, 1, 1, 482 | Rectangle: 483 | pos: self.pos 484 | size: self.size 485 | texture: self.texture 486 | 487 | : 488 | plane: plane 489 | config: config 490 | config_container: config_container 491 | tb_objects: tb_objects 492 | tb_slides: tb_slides 493 | container_edit: (container_objects, container_slides, config_container) 494 | container_publish: (container_controls, ) 495 | capture: capture 496 | 497 | # toolbar links 498 | btn_panel_text: btn_panel_text 499 | btn_panel_image: btn_panel_image 500 | btn_savefile: btn_savefile 501 | 502 | FboCapture: 503 | id: capture 504 | Widget: 505 | canvas: 506 | Color: 507 | rgba: bg_color 508 | Rectangle: 509 | size: self.size 510 | 511 | MainPlane: 512 | id: plane 513 | ctrl: root 514 | scale_min: 0.20 515 | scale_max: 20 516 | on_touch_down: root.unselect() 517 | canvas: 518 | Color: 519 | rgba: 1, 0, 0, .5 520 | Line: 521 | points: root.selection_points 522 | 523 | # Toolbar 524 | BoxLayout: 525 | id: container_objects 526 | orientation: 'vertical' 527 | size_hint_x: None 528 | width: 64 529 | canvas: 530 | Color: 531 | rgb: tb_color 532 | Rectangle: 533 | source: 'data/toolbar_left.png' 534 | size: self.size 535 | pos: self.pos 536 | 537 | BoxLayout: 538 | id: tb_objects 539 | orientation: 'vertical' 540 | size_hint_y: None 541 | 542 | ToolbarButton: 543 | id: btn_panel_text 544 | group: 'planeobject_adder' 545 | image: 'text' 546 | callback: root.toggle_text_panel 547 | no_toggle: False 548 | size: 56, 56 549 | 550 | ToolbarButton: 551 | id: btn_panel_image 552 | group: 'planeobject_adder' 553 | image: 'image' 554 | callback: root.toggle_localfile_panel 555 | no_toggle: False 556 | size: 56, 56 557 | 558 | Widget: 559 | 560 | BoxLayout: 561 | orientation: 'vertical' 562 | 563 | ToolbarButton: 564 | id: btn_savefile 565 | image: 'save' 566 | callback: root.do_save 567 | no_toggle: True 568 | size: 56, 56 569 | 570 | ToolbarButton: 571 | image: 'eye' 572 | callback: root.do_publish 573 | no_toggle: True 574 | size: 56, 56 575 | 576 | ToolbarButton: 577 | image: 'lock' 578 | callback: root.toggle_lock 579 | no_toggle: False 580 | size: 56, 56 581 | 582 | ToolbarButton: 583 | image: 'left' 584 | callback: root.ask_quit 585 | no_toggle: True 586 | size: 56, 56 587 | 588 | # Config menu 589 | StencilView: 590 | id: config_container 591 | size_hint_x: None 592 | x: 56 593 | FloatLayout: 594 | height: root.height 595 | x: 56 596 | id: config 597 | 598 | 599 | # Slides 600 | BoxLayout: 601 | id: container_slides 602 | orientation: 'vertical' 603 | size_hint_x: None 604 | width: 56 605 | pos_hint: {'right': 1} 606 | 607 | canvas: 608 | Color: 609 | rgba: tb_color 610 | Rectangle: 611 | source: 'data/toolbar_right.png' 612 | size: (64, self.height) 613 | pos: (self.x - (64 - self.width), self.y) 614 | 615 | ToolbarButton: 616 | image: 'add' 617 | callback: root.create_slide 618 | no_toggle: True 619 | size: 56, 56 620 | 621 | ScrollView: 622 | scroll_timeout: 100 623 | StackLayout: 624 | size_hint_y: None 625 | id: tb_slides 626 | 627 | ToolbarButton: 628 | image: 'refresh' 629 | callback: root.update_slides_capture 630 | no_toggle: True 631 | size: 56, 56 632 | 633 | # Player 634 | BoxLayout: 635 | id: container_controls 636 | size_hint_y: None 637 | 638 | ToolbarButton: 639 | image: 'left' 640 | callback: root.go_previous_slide 641 | no_toggle: True 642 | size: 64, 64 643 | color: tb_color[:3] + [.6] 644 | 645 | Widget 646 | 647 | ToolbarButton: 648 | image: 'eye' 649 | callback: root.go_return_action 650 | no_toggle: True 651 | size: 64, 64 652 | color: tb_color[:3] + [.6] 653 | 654 | Widget 655 | 656 | ToolbarButton: 657 | image: 'right' 658 | callback: root.go_next_slide 659 | no_toggle: True 660 | size: 64, 64 661 | color: tb_color[:3] + [.6] 662 | 663 | 664 | 665 | : 666 | canvas: 667 | Color: 668 | rgba: 0, 0, 0, self.alpha * 0.9 669 | Rectangle: 670 | pos: self.pos 671 | size: self.size 672 | BoxLayout: 673 | spacing: 20 674 | size: 450, 50 675 | size_hint: None, None 676 | center_y: root.top - root.center_y * root.alpha 677 | center_x: root.center_x 678 | 679 | Button: 680 | text: 'Return to edit' 681 | on_press: root.app.leave_quit() 682 | 683 | Button: 684 | text: 'Discard changes' 685 | on_press: root.app.do_save(); root.app.ask_quit(force=True) 686 | 687 | Button: 688 | text: 'Save' 689 | on_press: root.app.ask_quit(force=True) 690 | 691 | --------------------------------------------------------------------------------