from kivy.uix.boxlayout import BoxLayout from kivy.uix.label import Label from kivy.uix.screenmanager import Screen from kivy.uix.widget import Widget from kivy.uix.carousel import Carousel from kivy.uix.scrollview import ScrollView from kivy.uix.gridlayout import GridLayout from kivy.uix.stacklayout import StackLayout from kivy.uix.behaviors import FocusBehavior from kivy.uix.modalview import ModalView from kivy.uix.popup import Popup from kivy.core.window import Window from kivy.properties import (ObjectProperty, NumericProperty, OptionProperty, BooleanProperty, StringProperty, ListProperty) from kivy.animation import Animation from kivy.app import App from kivy import platform from kivy.lang import Builder from kivy.clock import Clock from time import time import traceback if platform == 'android': from interpreterwrapper import InterpreterWrapper import pydoc_data from jediinterface import get_completions, get_defs else: from pyonic.interpreterwrapper import InterpreterWrapper from pyonic.jediinterface import get_completions, get_defs import menu Builder.load_file('interpreter.kv') from widgets import ColouredButton import sys from os.path import realpath, join, dirname class NonDefocusingBehavior(object): def on_touch_down(self, touch): if self.collide_point(*self.to_parent(*touch.pos)): FocusBehavior.ignored_touch.append(touch) return super(NonDefocusingBehavior, self).on_touch_down(touch) class InitiallyFullGridLayout(GridLayout): '''A GridLayout that always contains at least one Widget, then makes that Widget as small as possible for self.minimum_height to exceed self.height by at least self.filling_widget_minimum_height + 1 pixel. ''' filling_widget_height = NumericProperty() filling_widget_minimum_height = NumericProperty(0) def on_parent(self, instance, value): self.parent.bind(height=self.calculate_filling_widget_height) # def on_height(self, instance, value): # if self.filling_widget_height > 1.5: # self.calculate_filling_widget_height() def on_minimum_height(self, instance, value): self.calculate_filling_widget_height() def calculate_filling_widget_height(self, *args): child_sum = sum([c.height for c in self.children[:-1]]) self.filling_widget_height = max(self.filling_widget_minimum_height, self.parent.height - child_sum) + 1. class NoTouchCarousel(Carousel): '''A carousel that doesn't let the user scroll with touch.''' def on_touch_down(self, touch): for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def _start_animation(self, *args, **kwargs): # compute target offset for ease back, next or prev new_offset = 0 direction = kwargs.get('direction', self.direction) is_horizontal = direction[0] in ['r', 'l'] extent = self.width if is_horizontal else self.height min_move = kwargs.get('min_move', self.min_move) _offset = kwargs.get('offset', self._offset) if _offset < min_move * -extent: new_offset = -extent elif _offset > min_move * extent: new_offset = extent if 'new_offset' in kwargs: new_offset = kwargs['new_offset'] # if new_offset is 0, it wasnt enough to go next/prev dur = self.anim_move_duration if new_offset == 0: dur = self.anim_cancel_duration # detect edge cases if not looping len_slides = len(self.slides) index = self.index if not self.loop or len_slides == 1: is_first = (index == 0) is_last = (index == len_slides - 1) if direction[0] in ['r', 't']: towards_prev = (new_offset > 0) towards_next = (new_offset < 0) else: towards_prev = (new_offset < 0) towards_next = (new_offset > 0) if (is_first and towards_prev) or (is_last and towards_next): new_offset = 0 anim = Animation(_offset=new_offset, d=dur, t=self.anim_type) anim.cancel_all(self) def _cmp(*l): if self._skip_slide is not None: self.index = self._skip_slide self._skip_slide = None anim.bind(on_complete=_cmp) anim.start(self) class OutputLabel(Label): stream = OptionProperty('stdout', options=['stdout', 'stderr']) class NonDefocusingScrollView(NonDefocusingBehavior, ScrollView): pass class InputLabel(Label): index = NumericProperty(0) root = ObjectProperty() blue_shift = NumericProperty(0.) blue_anim = Animation(blue_shift=0., t='out_expo', duration=0.5) def flash(self): self.blue_shift = 1. self.blue_anim.start(self) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return super(InputLabel, self).on_touch_down(touch) self.flash() self.root.insert_previous_code(self.index) return True class UserMessageLabel(Label): background_colour = ListProperty([1, 1, 0, 1]) class DocLabel(Label): background_colour = ListProperty([1, 0.922, 0.478, 1]) double_opacity = NumericProperty(1) def remove(self): anim = Animation(height=0, double_opacity=0, d=0.9, t='out_expo') anim.bind(on_complete=self._remove) anim.start(self) def _remove(self, *args): self.parent.remove_widget(self) class NotificationLabel(Label): background_colour = ListProperty([1, 0, 0, 0.5]) class NonDefocusingButton(NonDefocusingBehavior, ColouredButton): pass # def on_touch_down(self, touch): # if self.collide_point(*touch.pos): # FocusBehavior.ignored_touch.append(touch) # return super(NonDefocusingButton, self).on_touch_down(touch) class KeyboardButton(NonDefocusingBehavior, ColouredButton): pass # def on_touch_down(self, touch): # if self.collide_point(*touch.pos): # FocusBehavior.ignored_touch.append(touch) # return super(KeyboardButton, self).on_touch_down(touch) class InterpreterScreen(Screen): pass from kivy.uix.codeinput import CodeInput as InputWidget class InterpreterInput(InputWidget): '''TextInput styled for the app. This also overrides normal disabled behaviour to allow the widget to retain focus even when disabled, although input is still disabled. ''' root = ObjectProperty() trigger_completions = BooleanProperty(True) def __init__(self, *args, **kwargs): super(InterpreterInput, self).__init__(*args, **kwargs) self.register_event_type('on_request_completions') self.register_event_type('on_clear_completions') self.register_event_type('on_get_completions') if platform != 'android': from pygments.lexers import PythonLexer self.lexer = PythonLexer() App.get_running_app().bind(on_pause=self.on_pause) # self.text = '''for i in range(5): # print(i) # time.sleep(1)''' def on_request_completions(self): pass def on_clear_completions(self): pass def on_get_completions(self, text): pass def on_pause(self, *args): self.focus = False def on_disabled(self, instance, value): if value: from kivy.base import EventLoop self._hide_handles(EventLoop.window) def _on_focusable(self, instance, value): if not self.is_focusable: self.focus = False def on_cursor(self, instance, value): super(InterpreterInput, self).on_cursor(instance, value) self.get_completions() def get_completions(self, extra_text=''): row_index, line, col_index = self.currently_edited_line() if col_index == 0: self.dispatch('on_clear_completions') return completion_breakers = [' ', '.', ',', '(', ')', ':', '#', '[', ']', '{', '}', '&', '\\', '/', '+', '*', '\'', '\"', '|', '=', '^', '%'] if line[col_index - 1] in completion_breakers: self.dispatch('on_clear_completions') return if col_index + 1 < len(line) and line[col_index] not in completion_breakers: self.dispatch('on_clear_completions') return if not self.trigger_completions: self.dispatch('on_clear_completions') return self.dispatch('on_get_completions', extra_text) def currently_edited_line(self): '''Returns the row number, line text and column number for the current cursor pos.''' index = self.cursor_index() lines = self.text.split('\n') cur_num = 0 for i, line in enumerate(lines): line_length = len(line) + 1 # The +1 is for the deleted \n if cur_num + line_length > index: return i, line, index - cur_num cur_num += line_length raise ValueError('Could not identify currently edited line') # TODO: make not an error def insert_text(self, text, from_undo=False): if self.disabled: return if text != '\n' or self.text == '': return super(InterpreterInput, self).insert_text(text, from_undo=from_undo) self.dispatch('on_clear_completions') print(self.text.split('\n')) last_line = self.text.split('\n')[-1].rstrip() if len(last_line) == 0: return super(InterpreterInput, self).insert_text(text, from_undo=from_undo) num_spaces = len(last_line) - len(last_line.lstrip()) if last_line[-1] == ':': return super(InterpreterInput, self).insert_text(text + (num_spaces + 4) * ' ', from_undo=from_undo) else: return super(InterpreterInput, self).insert_text(text + num_spaces * ' ', from_undo=from_undo) def keyboard_on_key_down(self, window, keycode, text, modifiers): if keycode[1] == 'enter' and 'shift' in modifiers: self.root.interpret_line_from_code_input() return super(InterpreterInput, self).keyboard_on_key_down( window, keycode, text, modifiers) class InterpreterGui(BoxLayout): output_window = ObjectProperty() code_input = ObjectProperty() scrollview = ObjectProperty() input_fail_alpha = NumericProperty(0.) lock_input = BooleanProperty(False) _lock_input = BooleanProperty(False) halting = BooleanProperty(False) '''True when the interpreter has been asked to stop but has not yet done so.''' interpreter_state = OptionProperty('waiting', options=['waiting', 'interpreting', 'not_responding', 'restarting']) status_label_colour = StringProperty('b2ade6') _output_label_queue = ListProperty([]) dequeue_scheduled = ObjectProperty(None, allownone=True) clear_scheduled = ObjectProperty(None, allownone=True) awaiting_label_display_completion = BooleanProperty(False) throttle_label_output = BooleanProperty() '''Whether to clear the output label queue regularly. If False, labels will always be displayed, but this *will* cause problems with e.g. a constantly printing while loop. ''' interpreted_lines = ListProperty([]) '''A list of the lines of code that have been executed so far.''' completion_threads = [] '''The threads running jedi completion functions.''' most_recent_completion_time = 0. '''The most recent timestamp from a completion. New completions with older timestamps will be ignored.''' def __init__(self, *args, **kwargs): super(InterpreterGui, self).__init__(*args, **kwargs) self.animation = Animation(input_fail_alpha=0., t='out_expo', duration=0.5) self.interpreter = InterpreterWrapper( 'Interpreter', use_thread=True, throttle_output=App.get_running_app().setting__throttle_output, thread_name='interpreter') self.interpreter.bind(interpreter_state=self.setter('interpreter_state')) self.interpreter.bind(lock_input=self.setter('lock_input')) self.interpreter.bind(on_execution_complete=self.execution_complete) self.interpreter.bind(on_stdout=self.on_stdout) self.interpreter.bind(on_stderr=self.on_stderr) self.interpreter.bind(on_notification=self.on_notification) self.interpreter.bind(on_user_message=self.on_user_message) self.interpreter.bind(on_missing_labels=self.on_missing_labels) self.interpreter.bind(on_request_input=self.on_request_input) # self.interpreter = DummyInterpreter() # Clock.schedule_interval(self._dequeue_output_label, 0.05) # Clock.schedule_interval(self._clear_output_label_queue, 1) Clock.schedule_once(self.post_init_check, 0) def post_init_check(self, *args): if App.get_running_app().ctypes_working: return self.add_user_message_label( ('Could not load ctypes on this device. Keyboard interrupt ' 'will not be available.'), background_colour=(1, 0.6, 0, 1)) def on_lock_input(self, instance, value): if value: self.input_focus_on_disable = self.code_input.focus self._lock_input = True else: self._lock_input = False self.code_input.focus = self.input_focus_on_disable self.ensure_no_ctrl_c_button() self.halting = False def on_stdout(self, interpreter, text): self.add_output_label(text, 'stdout') def on_stderr(self, interpreter, text): self.add_output_label(text, 'stderr') def on_notification(self, interpreter, text): self.add_notification_label(text) def on_user_message(self, interpreter, text): self.add_user_message_label(text, background_colour=(1, 0.6, 0, 1)) def on_request_input(self, interpreter, prompt): self.show_input_popup(prompt) def show_input_popup(self, prompt): # Window.softinput_mode = 'below_target' p = InputPopup(prompt=prompt, submit_func=self.send_input) p.open() def send_input(self, text): '''Send the given input to the Python interpreter.''' self.interpreter.send_input(text) def ensure_ctrl_c_button(self): if not App.get_running_app().ctypes_working: return Clock.schedule_once(self._switch_to_ctrl_c_button, 0.4) def _switch_to_ctrl_c_button(self, *args): c = self.ids.carousel if c.index == 0: c.load_next() def clear_output(self): for child in self.output_window.children[:-1]: self.output_window.remove_widget(child) def exec_file(self): App.get_running_app().root.switch_to( 'filechooser', open_method=self._exec_file, success_screen_name='interpreter', purpose='exec file') def _exec_file(self, filename): self.add_user_message_label('Executing {}...'.format(filename)) self.ensure_ctrl_c_button() self.interpreter.exec_file(filename) def ensure_no_ctrl_c_button(self): Clock.unschedule(self._switch_to_ctrl_c_button) c = self.ids.carousel if c.index == 1: c.load_previous() else: Animation.cancel_all(c) c._start_animation(new_offset=0) def on_interpreter_state(self, instance, value): if value == 'waiting': self.status_label_colour = 'b2ade6' elif value == 'interpreting': self.status_label_colour = 'ade6b4' elif value == 'not_responding': self.status_label_colour = 'e6adad' elif value == 'restarting': self.status_label_colour = 'e6adad' def interpret_line_from_code_input(self): text = self.code_input.text if text == '': self.flash_input_fail() return self.code_input.text = '' self.interpret_line(text) def flash_input_fail(self): self.animation.stop(self) self.input_fail_alpha = 1. self.animation.start(self) def interpret_line(self, text): self.interpreted_lines.append(text) index = self.interpreter.interpret_line(text) self.add_input_label(text, index) self.ensure_ctrl_c_button() def add_user_message_label(self, text, **kwargs): l = UserMessageLabel(text=text, **kwargs) self.output_window.add_widget(l) self.scrollview.scroll_to(l) def add_doc_label(self, text, **kwargs): l = DocLabel(text=text, **kwargs) self.output_window.add_widget(l) self.scrollview.scroll_to(l) def add_input_label(self, text, index): l = InputLabel(text=text, index=index, root=self) self.output_window.add_widget(l) self.scrollview.scroll_to(l) def add_output_label(self, text, stream='stdout'): self._output_label_queue.append((text, stream)) # self._dequeue_output_label(0) def _add_output_label(self, text, stream='stdout', scroll_to=True): l = OutputLabel(text=text, stream=stream) self.output_window.add_widget(l) if scroll_to: self.scrollview.scroll_to(l) return l def _dequeue_output_label(self, dt): if not self._output_label_queue: return # print('dequeueing', self._output_label_queue) t = time() i = 0 while (time() - t) < 0.005: i += 1 if not self._output_label_queue: break label_text = self._output_label_queue.pop(0) label = self._add_output_label(*label_text, scroll_to=False) print('Rendered {} labels in {}'.format(i, time() - t)) Animation.stop_all(self.scrollview, 'scroll_x', 'scroll_y') self.scrollview.scroll_to(label) self.dequeue_scheduled.cancel() self.dequeue_scheduled = None if len(self._output_label_queue) == 0 and self.clear_scheduled: self.clear_scheduled.cancel() self.clear_scheduled = None elif len(self._output_label_queue) > 0: self.dequeue_scheduled = Clock.schedule_once( self._dequeue_output_label, 0.05) if (self.awaiting_label_display_completion and len(self._output_label_queue) == 0): self.awaiting_label_display_completion = False self._execution_complete() def _clear_output_label_queue(self, dt): if not self.throttle_label_output: return labels = self._output_label_queue self._output_label_queue = [] if labels: self.add_missing_labels_marker(labels=labels) if self.dequeue_scheduled: self.dequeue_scheduled.cancel() self.dequeue_scheduled = None if self.clear_scheduled: self.clear_scheduled.cancel() self.clear_scheduled = None if self.awaiting_label_display_completion: self.awaiting_label_display_completion = False self._execution_complete() def on__output_label_queue(self, instance, values): # print('olq', self.dequeue_scheduled, self.clear_scheduled) if self.dequeue_scheduled: return if not self.dequeue_scheduled: self.dequeue_scheduled = Clock.schedule_once(self._dequeue_output_label, 0) if not self.clear_scheduled: self.clear_scheduled = Clock.schedule_once( self._clear_output_label_queue, 1) def on_throttle_label_output(self, instance, value): self.interpreter.set_service_output_throttling(value) def on_missing_labels(self, instance, number): self.add_missing_labels_marker(num_labels=number) def add_missing_labels_marker(self, num_labels=None, labels=None): if labels is not None: num_labels = len(labels) self.add_user_message_label( '{} lines omitted (too many to render)'.format(num_labels), background_colour=(1, 0.6, 0, 1)) # l.labels = labels def add_notification_label(self, text): self.add_break() l = NotificationLabel(text=text) self.output_window.add_widget(l) self.scrollview.scroll_to(l) self.add_break() def add_break(self): b = BreakMarker() self.output_window.add_widget(b) self.scrollview.scroll_to(b) def insert_previous_code(self, index, clear=False): if clear: self.code_input.text = '' code = self.interpreter.inputs[index] if self.code_input.text == '': self.code_input.text = code else: self.code_input.text += '\n' + code def send_sigint(self): self.halting = True self.interpreter.send_sigint() def restart_interpreter(self): self.interpreted_lines = [] self.interpreter.restart() def query_restart(self): popup = RestartPopup(interpreter_gui=self) popup.open() def execution_complete(self, *args): '''Called when execution is complete so the TextInput should be unlocked etc., but first this is delayed until messages finish printing. ''' if len(self._output_label_queue) == 0: self._execution_complete() else: self.awaiting_label_display_completion = True def _execution_complete(self): self.add_break() self.lock_input = False self.halting = False self.ensure_no_ctrl_c_button() def get_defs(self): previous_text = '\n'.join(self.interpreted_lines) num_previous_lines = len(previous_text.split('\n')) text = self.code_input.text row_index, line, col_index = self.code_input.currently_edited_line() get_defs('\n'.join([previous_text, text]), self.show_defs, line=row_index + num_previous_lines + 1, col=col_index) def show_defs(self, defs, sigs, error=None): print('docs are', defs) if error is not None: self.add_doc_label(error) return if not defs and not sigs: self.add_doc_label('No definition found at cursor') return if defs: d = defs[0] else: d = sigs[0] if hasattr(d, 'params'): text = '{}({})\n{}'.format(d.desc_with_module, ', '.join([p.description for p in d.params]), d.doc) else: text = '{}\n{}'.format(d.desc_with_module, d.doc) self.add_doc_label(text) def check_completion_threads(self): for thread in self.completion_threads: if not thread.is_alive(): self.completion_threads.remove(thread) def get_completions(self, extra_text=''): if not self.enable_autocompletion: return previous_text = '\n'.join(self.interpreted_lines) num_previous_lines = len(previous_text.split('\n')) text = self.code_input.text row_index, line, col_index = self.code_input.currently_edited_line() self.check_completion_threads() if len(self.completion_threads) > 3: return thread = get_completions('\n'.join([previous_text, text + extra_text]), self.show_completions, line=row_index + num_previous_lines + 1, col=col_index) self.completion_threads.append(thread) def show_completions(self, completions, time=None): if time is not None: if time < self.most_recent_completion_time: return self.most_recent_completion_time = time self.ids.completions.completions = completions # self.ids.completions.text = ', '.join(completions[:5]) # print('set text') def clear_completions(self): self.most_recent_completion_time = time() # block running completions self.ids.completions.completions = [] class CompletionButton(KeyboardButton): interpreter_gui = ObjectProperty() completion_type = StringProperty() completion_name = StringProperty() completion_complete = StringProperty() completion = ObjectProperty() colour_mappings = {'keyword': (0, 0.8, 0, 0.1), 'instance': (0.8, 0, 0, 0.1), 'function': (0, 0, 0.8, 0.1), 'module': (0.8, 0.8, 0, 0.1)} def on_release(self): self.interpreter_gui.ids.completions.completions = [] self.interpreter_gui.code_input.trigger_completions = False for letter in self.completion.complete: self.interpreter_gui.code_input.insert_text(letter) if App.get_running_app().setting__autocompletion_brackets: if self.completion.type in ('function', 'class'): if not (self.completion.name == 'print' and sys.version_info.major == 2): # This check is necessary as jedi detects print as a # function in Python 2 on Android (maybe because of # the __future__ import in another file?) self.interpreter_gui.code_input.insert_text('(') self.interpreter_gui.code_input.insert_text(')') self.interpreter_gui.code_input.do_cursor_movement('cursor_left') self.interpreter_gui.code_input.trigger_completions = True def on_completion(self, instance, completion): self.completion_type = completion.type self.completion_name = completion.name self.completion_complete = completion.complete class CompletionsList(StackLayout): completions = ListProperty([]) completion_type = StringProperty() completion_completion = StringProperty() interpreter_gui = ObjectProperty() def on_completions(self, instance, completions): # self.clear_widgets() if len(completions) > 5: print('too many completions, not showing options') self.clear_widgets() removals = self.children[:] for completion in completions[:5]: if not completion.complete: continue for widget in self.children: if widget.completion.name == completion.name: widget.completion = completion removals.remove(widget) break else: try: button = CompletionButton( # text=completion.name, completion=completion, interpreter_gui=self.interpreter_gui) self.add_widget(button) except: print('Exception when making CompletionButton from {}'.format( completion)) traceback.print_exc() for removal in removals: self.remove_widget(removal) def on_width(self, instance, value): self.on_minimum_width(self, self.minimum_width) def on_minimum_width(self, instance, value): while sum([c.width for c in self.children]) > self.width: self.remove_widget(self.children[0]) class RestartPopup(ModalView): interpreter_gui = ObjectProperty() class BreakMarker(Widget): pass class InputPopup(Popup): prompt = StringProperty() submit_func = ObjectProperty() def submit_text(self, text): self.submit_func(text) # Window.softinput_mode = 'pan' self.dismiss() # This is the normal ModalView on_touch_down, with # self.submit_func added to ensure that some text is submitted. def on_touch_down(self, touch): if not self.collide_point(*touch.pos): if self.auto_dismiss: self.submit_func(self.ids.ti.text) self.dismiss() return True super(ModalView, self).on_touch_down(touch) return True # Again, modify the normal _handle_keyboard so that # self.submit_func is called before self.dismiss def _handle_keyboard(self, window, key, *largs): if key == 27 and self.auto_dismiss: self.submit_func(self.ids.ti.text) self.dismiss() return True class InterpreterMenuDropDown(menu.MenuDropDown): pass class InterpreterMenuButton(menu.MenuButton): dropdown_cls = ObjectProperty(InterpreterMenuDropDown)