class Spotlight(App): ''' This class represents the kivy app that will run the spotlight ''' def __init__(self, **kwargs): super(Spotlight, self).__init__(**kwargs) # fixed width of the app, will never change self._width = kwargs.get('width', 500) # tells how many entries can be displayed on the screen at the same time. The rest will be accessible via a scroller self._max_results_displayed = kwargs.get('max_results_displayed', 5) # gives the height of the separators that will be used between each entry self._sep_height = kwargs.get('sep_height', 1) # static height of the main search bar: SearchInput self._search_field_height = kwargs.get('search_field_height', 35) # height of each entry self._result_height = kwargs.get('result_height', 25) # this is the spacing between the search bar and the scroller containing the entries self._spacing = kwargs.get('spacing', 1) # this is the padding of the main window self._padding = 5 # this is the space between each separator/result in the dropdown list self._result_spacing = kwargs.get('result_spacing', 1) # color of a non-selected entry self._inactive_button_color = (.0,.0,.0, 0) # color of a selected entry self._active_button_color = (.4, .4, .4, 1) # store all the visible entries on the screen as a pair (button, separator) for convenience self._results = [] # index of the result that is currently highlighted self._highlight_index = -1 # these 3 variables are the 3 callbacks that the controller can input self._on_build = None self._on_enter = None self._on_text = None # this field holds a preset number of buttons for efficiency self._button_pool = [] # parse the callbacks passed in the constructor self.user_bind(**kwargs) self.build_window() # update the window size self.update_window() def user_bind(self, **kwargs): ''' this function saves the callbacks passed to this function into the 3 available holders ''' # this event is triggered when the application is drawn for the first time self._on_build = kwargs.get('on_build', self._on_build) # this event is triggered when the user presses enter self._on_enter = kwargs.get('on_enter', self._on_enter) # this even is triggered whenever the text in the search field is changed self._on_text = kwargs.get('on_text', self._on_text) def on_start(self): ''' when the window is drawn and the application started we update the size of the window ''' self.update_window() def build_window(self): ''' this function builds the whole app ''' self._search_field = SearchInput(multiline=False, focus=True, realdonly=False, height=self._search_field_height, size_hint=(1, None), markup=True, valign='middle', font_size = 20, font_name = 'data/fonts/DejaVuSans.ttf') self._search_field.bind(focus=self._on_focus) self._search_field._keyboard.bind(on_key_down=self._on_keyboard_down) self._search_field.background_active = '' self._search_field.font_size = 20 self._search_field.bind(on_text_validate = self._on_text_validate) self._search_field.bind(text = self._on_new_text) self._search_field.text = '' self._drop_down_list = GridLayout(cols=1, width=self._width, spacing=self._result_spacing, size_hint = (None, None)) self._drop_down_list.bind(minimum_height = self._drop_down_list.setter('height')) self._scroller = ScrollView(scroll_distance=10, scroll_type=['bars'], do_scroll_x=False, bar_width=10, size_hint=(1, 1)) self._scroller.add_widget(self._drop_down_list) self._layout = ColoredGridLayout(cols=1, width=self._width, height=self._search_field_height, padding=(self._padding, self._padding), spacing=self._spacing*2) self._layout.add_widget(self._search_field) self._layout.add_widget(self._scroller) if self._on_build: self._on_build() return self._layout def build(self): return self._layout def _unbind_all(self): self._search_field.unbind(focus=self._on_focus) self._search_field._keyboard.unbind(on_key_down=self._on_keyboard_down) self._search_field.unbind(on_text_validate = self._on_text_validate) self._search_field.unbind(text = self._on_new_text) self._drop_down_list.unbind(minimum_height = self._drop_down_list.setter('height')) for btn in self._button_pool: btn.unbind(width = button_width_setter) btn.unbind(on_press=self._on_click) btn.unbind(texture_size=btn.setter('text_size')) def _on_new_text(self, value, text): if self._on_text: self._on_text(self, value, text) def _on_text_validate(self, value): ''' when the user pressed enter, we forward the callback to the controller with the current hightlight index ''' if self._on_enter: ret = self._on_enter(value, self._highlight_index) if ret: self._unbind_all() self.stop() def _on_focus(self, instance, value): ''' this function is called whenever the focus of the search field changes. We do NOT allow defocus ''' if not value: self._search_field.focus = True # since the search field has to re-claim the keyboard, we re-bind our callback self._search_field._keyboard.bind(on_key_down=self._on_keyboard_down) def update_window(self, *args): ''' based on the current amount of entries shown, we adapt the size of the window ''' result_count = len(self._results) win_width = 2*self._padding + self._width win_height = 2*self._padding + self._search_field_height + self._spacing + (self._result_spacing * 2 + self._result_height + self._sep_height) * result_count max_height = 2*self._padding + self._search_field_height + self._spacing + (self._result_spacing * 2 + self._result_height + self._sep_height) * self._max_results_displayed if self._app_window: self._app_window.size = win_width, min(win_height, max_height) def _on_keyboard_down(self, keyboard, keycode, text, modifiers): ''' we handle 3 keys: up (resp. down) to hightlight the entry above (resp. below) and escape to quit the application ''' if keycode[1] == 'up': self._highlight_up() elif keycode[1] == 'down': self._highlight_down() elif keycode[1] == 'escape': keyboard.release() self._unbind_all() self.stop() else: # mark the key press as not handled return False # mark the key press as handled return True def pre_allocate(self, number): self._button_pool = [] for _ in range(0, number): btn = Button(text='str', height=self._result_height, size_hint=(1, None), valign='middle', halign='left', background_color=self._inactive_button_color, markup = True, padding_x = 0) btn.bind(width = button_width_setter) btn.bind(on_press=self._on_click) btn.bind(texture_size=btn.setter('text_size')) btn.background_normal = '' btn.background_down = btn.background_normal self._button_pool.append(btn) def _build_button(self): if self._button_pool: return self._button_pool.pop() btn = Button(text='str', height=self._result_height, size_hint=(1, None), valign='middle', halign='left', background_color=self._inactive_button_color, markup = True, padding_x = 0) btn.bind(width = button_width_setter) btn.bind(on_press=self._on_click) btn.bind(texture_size=btn.setter('text_size')) btn.background_normal = '' btn.background_down = btn.background_normal return btn def _release_button(self, btn): btn.background_color = self._inactive_button_color self._button_pool.append(btn) def add_result(self, str, redraw = True): ''' add a new entry to the dropdown list; an index is returned ''' btn = self._build_button() btn.text = str sep = Separator(height = self._sep_height) self._drop_down_list.add_widget(sep) self._drop_down_list.add_widget(btn) self._results.append((btn, sep)) # we reset the highlight self._highlight_reset() if redraw: self.update_window() return len(self._results)-1 def get_result(self, idx): ''' get a button object from an index - returned from a previous call to add_result ''' if not idx < len(self._results) or not idx >= 0: return e, _ = self._results[idx] return e def remove_result(self, idx, redraw = True): ''' remove a result object from its index - returned from a previous call to add_result ''' if not idx < len(self._results) or not idx >= 0: return e, sep = self._results[idx] if sep: self._drop_down_list.remove_widget(sep) self._drop_down_list.remove_widget(e) self._results.remove((e, sep)) self._release_button(e) # we reset the highlight self._highlight_reset() # resize the window accordingly if redraw: self.update_window() def clear_results(self, redraw = True): ''' clear all the results ''' for e, sep in self._results: self._release_button(e) self._drop_down_list.clear_widgets() self._results = [] # we reset the highlight self._highlight_reset() # resize the window accordingly if redraw: self.update_window() def _on_click(self, instance): ''' this callback is called whenever a click on a result is done; the highlight is adapted ''' for i in range(0, len(self._results)): e, _ = self._results[i] if e is instance: offset = i-self._highlight_index self._highlight_update(offset) self._on_text_validate(1) break def _scroll_update(self): ''' this function adapts the scroller to ensure that the highlighted object is visible ''' highlight_reverse_index = len(self._results) - 1 - self._highlight_index item_lb = highlight_reverse_index * (self._result_spacing*2 + self._sep_height + self._result_height) item_ub = item_lb + self._result_height + self._result_spacing*2 + self._sep_height view_size = (self._result_spacing * 2 + self._result_height + self._sep_height) * self._max_results_displayed total_size = (self._result_spacing * 2 + self._result_height + self._sep_height) * len(self._results) lb = self._scroller.scroll_y * (total_size - view_size) ub = lb + view_size if item_lb < lb: self._scroller.scroll_y -= self._scroller.convert_distance_to_scroll(0, lb - item_lb)[1] elif item_ub > ub: self._scroller.scroll_y += self._scroller.convert_distance_to_scroll(0, item_ub - ub)[1] def _highlight_update(self, offset): ''' move the hightlight by `offset' amount ''' if self._highlight_index > -1 and self._highlight_index < len(self._results): e, sep = self._results[self._highlight_index] e.background_color = self._inactive_button_color self._highlight_index += offset self._highlight_index = min(self._highlight_index, len(self._results)-1) if self._results: self._highlight_index = max(self._highlight_index, 0) else: self._highlight_index = max(self._highlight_index, 1) if self._highlight_index > -1 and self._highlight_index < len(self._results): e, sep = self._results[self._highlight_index] e.background_color = self._active_button_color self._scroll_update() def _highlight_reset(self): offset = -self._highlight_index self._highlight_update(offset) def _highlight_up(self): self._highlight_update(-1) def _highlight_down(self): self._highlight_update(+1) def on_stop(self): pygame.display.quit() pass
class Spotlight(App): ''' This class represents the kivy app that will run the spotlight ''' def __init__(self, **kwargs): super(Spotlight, self).__init__(**kwargs) # fixed width of the app, will never change self._width = kwargs.get('width', 500) # tells how many entries can be displayed on the screen at the same time. The rest will be accessible via a scroller self._max_results_displayed = kwargs.get('max_results_displayed', 5) # gives the height of the separators that will be used between each entry self._sep_height = kwargs.get('sep_height', 1) # static height of the main search bar: SearchInput self._search_field_height = kwargs.get('search_field_height', 35) # height of each entry self._result_height = kwargs.get('result_height', 25) # this is the spacing between the search bar and the scroller containing the entries self._spacing = kwargs.get('spacing', 1) # this is the padding of the main window self._padding = 5 # this is the space between each separator/result in the dropdown list self._result_spacing = kwargs.get('result_spacing', 1) # color of a non-selected entry self._inactive_button_color = (.0, .0, .0, 0) # color of a selected entry self._active_button_color = (.4, .4, .4, 1) # store all the visible entries on the screen as a pair (button, separator) for convenience self._results = [] # index of the result that is currently highlighted self._highlight_index = -1 # these 3 variables are the 3 callbacks that the controller can input self._on_build = None self._on_enter = None self._on_text = None # this field holds a preset number of buttons for efficiency self._button_pool = [] # parse the callbacks passed in the constructor self.user_bind(**kwargs) self.build_window() # update the window size self.update_window() def user_bind(self, **kwargs): ''' this function saves the callbacks passed to this function into the 3 available holders ''' # this event is triggered when the application is drawn for the first time self._on_build = kwargs.get('on_build', self._on_build) # this event is triggered when the user presses enter self._on_enter = kwargs.get('on_enter', self._on_enter) # this even is triggered whenever the text in the search field is changed self._on_text = kwargs.get('on_text', self._on_text) def on_start(self): ''' when the window is drawn and the application started we update the size of the window ''' self.update_window() def build_window(self): ''' this function builds the whole app ''' self._search_field = SearchInput(multiline=False, focus=True, realdonly=False, height=self._search_field_height, size_hint=(1, None), markup=True, valign='middle', font_size=20, font_name='data/fonts/DejaVuSans.ttf') self._search_field.bind(focus=self._on_focus) self._search_field._keyboard.bind(on_key_down=self._on_keyboard_down) self._search_field.background_active = '' self._search_field.font_size = 20 self._search_field.bind(on_text_validate=self._on_text_validate) self._search_field.bind(text=self._on_new_text) self._search_field.text = '' self._drop_down_list = GridLayout(cols=1, width=self._width, spacing=self._result_spacing, size_hint=(None, None)) self._drop_down_list.bind( minimum_height=self._drop_down_list.setter('height')) self._scroller = ScrollView(scroll_distance=10, scroll_type=['bars'], do_scroll_x=False, bar_width=10, size_hint=(1, 1)) self._scroller.add_widget(self._drop_down_list) self._layout = ColoredGridLayout(cols=1, width=self._width, height=self._search_field_height, padding=(self._padding, self._padding), spacing=self._spacing * 2) self._layout.add_widget(self._search_field) self._layout.add_widget(self._scroller) if self._on_build: self._on_build() return self._layout def build(self): return self._layout def _unbind_all(self): self._search_field.unbind(focus=self._on_focus) self._search_field._keyboard.unbind(on_key_down=self._on_keyboard_down) self._search_field.unbind(on_text_validate=self._on_text_validate) self._search_field.unbind(text=self._on_new_text) self._drop_down_list.unbind( minimum_height=self._drop_down_list.setter('height')) for btn in self._button_pool: btn.unbind(width=button_width_setter) btn.unbind(on_press=self._on_click) btn.unbind(texture_size=btn.setter('text_size')) def _on_new_text(self, value, text): if self._on_text: self._on_text(self, value, text) def _on_text_validate(self, value): ''' when the user pressed enter, we forward the callback to the controller with the current hightlight index ''' if self._on_enter: ret = self._on_enter(value, self._highlight_index) if ret: self._unbind_all() self.stop() def _on_focus(self, instance, value): ''' this function is called whenever the focus of the search field changes. We do NOT allow defocus ''' if not value: self._search_field.focus = True # since the search field has to re-claim the keyboard, we re-bind our callback self._search_field._keyboard.bind( on_key_down=self._on_keyboard_down) def update_window(self, *args): ''' based on the current amount of entries shown, we adapt the size of the window ''' result_count = len(self._results) win_width = 2 * self._padding + self._width win_height = 2 * self._padding + self._search_field_height + self._spacing + ( self._result_spacing * 2 + self._result_height + self._sep_height) * result_count max_height = 2 * self._padding + self._search_field_height + self._spacing + ( self._result_spacing * 2 + self._result_height + self._sep_height) * self._max_results_displayed if self._app_window: self._app_window.size = win_width, min(win_height, max_height) def _on_keyboard_down(self, keyboard, keycode, text, modifiers): ''' we handle 3 keys: up (resp. down) to hightlight the entry above (resp. below) and escape to quit the application ''' if keycode[1] == 'up': self._highlight_up() elif keycode[1] == 'down': self._highlight_down() elif keycode[1] == 'escape': keyboard.release() self._unbind_all() self.stop() else: # mark the key press as not handled return False # mark the key press as handled return True def pre_allocate(self, number): self._button_pool = [] for _ in range(0, number): btn = Button(text='str', height=self._result_height, size_hint=(1, None), valign='middle', halign='left', background_color=self._inactive_button_color, markup=True, padding_x=0) btn.bind(width=button_width_setter) btn.bind(on_press=self._on_click) btn.bind(texture_size=btn.setter('text_size')) btn.background_normal = '' btn.background_down = btn.background_normal self._button_pool.append(btn) def _build_button(self): if self._button_pool: return self._button_pool.pop() btn = Button(text='str', height=self._result_height, size_hint=(1, None), valign='middle', halign='left', background_color=self._inactive_button_color, markup=True, padding_x=0) btn.bind(width=button_width_setter) btn.bind(on_press=self._on_click) btn.bind(texture_size=btn.setter('text_size')) btn.background_normal = '' btn.background_down = btn.background_normal return btn def _release_button(self, btn): btn.background_color = self._inactive_button_color self._button_pool.append(btn) def add_result(self, str, redraw=True): ''' add a new entry to the dropdown list; an index is returned ''' btn = self._build_button() btn.text = str sep = Separator(height=self._sep_height) self._drop_down_list.add_widget(sep) self._drop_down_list.add_widget(btn) self._results.append((btn, sep)) # we reset the highlight self._highlight_reset() if redraw: self.update_window() return len(self._results) - 1 def get_result(self, idx): ''' get a button object from an index - returned from a previous call to add_result ''' if not idx < len(self._results) or not idx >= 0: return e, _ = self._results[idx] return e def remove_result(self, idx, redraw=True): ''' remove a result object from its index - returned from a previous call to add_result ''' if not idx < len(self._results) or not idx >= 0: return e, sep = self._results[idx] if sep: self._drop_down_list.remove_widget(sep) self._drop_down_list.remove_widget(e) self._results.remove((e, sep)) self._release_button(e) # we reset the highlight self._highlight_reset() # resize the window accordingly if redraw: self.update_window() def clear_results(self, redraw=True): ''' clear all the results ''' for e, sep in self._results: self._release_button(e) self._drop_down_list.clear_widgets() self._results = [] # we reset the highlight self._highlight_reset() # resize the window accordingly if redraw: self.update_window() def _on_click(self, instance): ''' this callback is called whenever a click on a result is done; the highlight is adapted ''' for i in range(0, len(self._results)): e, _ = self._results[i] if e is instance: offset = i - self._highlight_index self._highlight_update(offset) self._on_text_validate(1) break def _scroll_update(self): ''' this function adapts the scroller to ensure that the highlighted object is visible ''' highlight_reverse_index = len( self._results) - 1 - self._highlight_index item_lb = highlight_reverse_index * ( self._result_spacing * 2 + self._sep_height + self._result_height) item_ub = item_lb + self._result_height + self._result_spacing * 2 + self._sep_height view_size = (self._result_spacing * 2 + self._result_height + self._sep_height) * self._max_results_displayed total_size = (self._result_spacing * 2 + self._result_height + self._sep_height) * len(self._results) lb = self._scroller.scroll_y * (total_size - view_size) ub = lb + view_size if item_lb < lb: self._scroller.scroll_y -= self._scroller.convert_distance_to_scroll( 0, lb - item_lb)[1] elif item_ub > ub: self._scroller.scroll_y += self._scroller.convert_distance_to_scroll( 0, item_ub - ub)[1] def _highlight_update(self, offset): ''' move the hightlight by `offset' amount ''' if self._highlight_index > -1 and self._highlight_index < len( self._results): e, sep = self._results[self._highlight_index] e.background_color = self._inactive_button_color self._highlight_index += offset self._highlight_index = min(self._highlight_index, len(self._results) - 1) if self._results: self._highlight_index = max(self._highlight_index, 0) else: self._highlight_index = max(self._highlight_index, 1) if self._highlight_index > -1 and self._highlight_index < len( self._results): e, sep = self._results[self._highlight_index] e.background_color = self._active_button_color self._scroll_update() def _highlight_reset(self): offset = -self._highlight_index self._highlight_update(offset) def _highlight_up(self): self._highlight_update(-1) def _highlight_down(self): self._highlight_update(+1) def on_stop(self): pygame.display.quit() pass