def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) show_recent_apps = self.settings.get_property('show-recent-apps') recent_apps_number = 3 if show_recent_apps else 0 try: recent_apps_number = int(str(show_recent_apps)) except ValueError: logger.warning("show-recent-apps in settings is not a number, fallback do default value") if not result_items and not self.input.get_text() and recent_apps_number > 0: result_items = AppStatDb.get_instance().get_most_frequent(recent_apps_number) results = self.create_item_widgets(result_items, self._get_user_query()) if results: self._results_render_time = time.time() for item in results: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) logger.debug('render %s results', len(results))
def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) if not result_items and not self.input.get_text( ) and self.settings.get_property('show-recent-apps'): result_items = AppStatDb.get_instance().get_most_frequent(3) results = self.create_item_widgets(result_items, self._get_user_query()) if results: self._results_render_time = time.time() for item in results: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) logger.debug('render %s results', len(results))
def show_results(self, results): """ :param list results: list of Result instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) limit = len(self.settings.get_jump_keys()) or 25 show_recent_apps = self.settings.get_property('show-recent-apps') recent_apps_number = int( show_recent_apps) if show_recent_apps.isnumeric() else 0 if not self.input.get_text() and recent_apps_number > 0: results = AppResult.get_most_frequent(recent_apps_number) results = self.create_item_widgets(results, self._get_user_query()) if results: for item in results[:limit]: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) self.scroll_container.show_all() else: # Hide the scroll container when there are no results since it normally takes up a # minimum amount of space even if it is empty. self.scroll_container.hide() logger.debug('render %s results', len(results))
def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) if not result_items and not self.input.get_text() and self.settings.get_property('show-recent-apps'): result_items = AppStatDb.get_instance().get_most_frequent(3) results = self.create_item_widgets(result_items, self._get_user_query()) if results: self._results_render_time = time.time() map(self.result_box.add, results) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) logger.debug('render %s results' % len(results))
def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) results = list(create_item_widgets(result_items, self.get_user_query())) # generator -> list if results: self._resultsRenderTime = time.time() map(self.result_box.add, results) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self.get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box, self.provider) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0)
class UlauncherWindow(Gtk.Window, WindowHelper): __gtype_name__ = "UlauncherWindow" _current_accel_name = None _results_render_time = 0 @classmethod @singleton def get_instance(cls): return cls() def __new__(cls): """Special static method that's automatically called by Python when constructing a new instance of this class. Returns a fully instantiated BaseUlauncherWindow object. """ builder = Builder.new_from_file('UlauncherWindow') new_object = builder.get_object("ulauncher_window") new_object.finish_initializing(builder) return new_object def finish_initializing(self, builder): """Called while initializing this instance in __new__ finish_initializing should be called after parsing the UI definition and creating a UlauncherWindow object with it in order to finish initializing the start of the new UlauncherWindow instance. """ # Get a reference to the builder and set up the signals. self.builder = builder self.ui = builder.get_ui(self, True) self.PreferencesDialog = None # class self.preferences_dialog = None # instance self.results_nav = None self.window = self.ui['ulauncher_window'] self.window_body = self.ui['body'] self.input = self.ui['input'] self.prefs_btn = self.ui['prefs_btn'] self.result_box = self.ui["result_box"] self.input.connect('changed', self.on_input_changed) self.prefs_btn.connect('clicked', self.on_mnu_preferences_activate) self.set_keep_above(True) self.PreferencesDialog = PreferencesUlauncherDialog self.settings = Settings.get_instance() self.fix_window_width() self.position_window() self.init_theme() # this will trigger to show frequent apps if necessary self.show_results([]) if not is_wayland_compatibility_on(): # bind hotkey Keybinder.init() accel_name = self.settings.get_property('hotkey-show-app') # bind in the main thread GLib.idle_add(self.bind_show_app_hotkey, accel_name) start_app_watcher() ExtensionServer.get_instance().start() time.sleep(0.01) ExtensionRunner.get_instance().run_all() if not get_options().no_extensions: ExtensionDownloader.get_instance().download_missing() ###################################### # GTK Signal Handlers ###################################### # pylint: disable=unused-argument def on_mnu_about_activate(self, widget, data=None): """Display the about page for ulauncher.""" self.activate_preferences(page='about') def on_mnu_preferences_activate(self, widget, data=None): """Display the preferences window for ulauncher.""" self.activate_preferences(page='preferences') def on_mnu_close_activate(self, widget, data=None): """Signal handler for closing the UlauncherWindow.""" self.destroy() def on_destroy(self, widget, data=None): """Called when the UlauncherWindow is closed.""" # Clean up code for saving application state should be added here. Gtk.main_quit() def on_preferences_dialog_destroyed(self, widget, data=None): '''only affects GUI logically there is no difference between the user closing, minimizing or ignoring the preferences dialog''' logger.debug('on_preferences_dialog_destroyed') # to determine whether to create or present preferences_dialog self.preferences_dialog = None def on_focus_out_event(self, widget, event): # apparently Gtk doesn't provide a mechanism to tell if window is in focus # this is a simple workaround to avoid hiding window # when user hits Alt+key combination or changes input source, etc. self.is_focused = False t = threading.Timer(0.07, lambda: self.is_focused or self.hide()) t.start() def on_focus_in_event(self, *args): if self.settings.get_property('grab-mouse-pointer'): ptr_dev = self.get_pointer_device() result = ptr_dev.grab( self.window.get_window(), Gdk.GrabOwnership.NONE, True, Gdk.EventMask.ALL_EVENTS_MASK, None, 0 ) logger.debug("Focus in event, grabbing pointer: %s", result) self.is_focused = True def on_input_changed(self, entry): """ Triggered by user input """ Search.get_instance().on_query_change(self._get_user_query()) # pylint: disable=inconsistent-return-statements def on_input_key_press_event(self, widget, event): keyval = event.get_keyval() keyname = Gdk.keyval_name(keyval[1]) alt = event.state & Gdk.ModifierType.MOD1_MASK Search.get_instance().on_key_press_event(widget, event, self._get_user_query()) if self.results_nav: if keyname in ('Up', 'ISO_Left_Tab'): self.results_nav.go_up() return True if keyname in ('Down', 'Tab'): self.results_nav.go_down() return True if alt and keyname in ('Return', 'KP_Enter'): self.enter_result_item(alt=True) elif keyname in ('Return', 'KP_Enter'): self.enter_result_item() elif alt and keyname.isdigit() and 0 < int(keyname) < 10: # on Alt+<num> try: self.enter_result_item(int(keyname) - 1) except IndexError: # selected non-existing result item pass elif alt and len(keyname) == 1 and 97 <= ord(keyname) <= 122: # on Alt+<char> try: self.enter_result_item(ord(keyname) - 97 + 9) except IndexError: # selected non-existing result item pass if keyname == 'Escape': self.hide() ###################################### # Helpers ###################################### def get_input(self): return self.input def fix_window_width(self): """ Add 2px to the window width if GTK+ >= 3.20 Because of the bug in <3.20 that doesn't add css borders to the width """ if gtk_version_is_gte(3, 20, 0): width, height = self.get_size_request() self.set_size_request(width + 2, height) def init_theme(self): load_available_themes() theme = Theme.get_current() theme.clear_cache() # removing window shadow solves issue with DEs without a compositor (#230) if get_options().no_window_shadow: self.window_body.get_style_context().add_class('no-window-shadow') self._render_prefs_icon() self.init_styles(theme.compile_css()) def activate_preferences(self, page='preferences'): """ From the PyGTK Reference manual Say for example the preferences dialog is currently open, and the user chooses Preferences from the menu a second time; use the present() method to move the already-open dialog where the user can see it. """ self.hide() if self.preferences_dialog is not None: logger.debug('show existing preferences_dialog') self.preferences_dialog.present(page=page) elif self.PreferencesDialog is not None: logger.debug('create new preferences_dialog') self.preferences_dialog = self.PreferencesDialog() # pylint: disable=E1102 self.preferences_dialog.connect('destroy', self.on_preferences_dialog_destroyed) self.preferences_dialog.show(page=page) # destroy command moved into dialog to allow for a help button def position_window(self): window_width = self.get_size()[0] screen = get_current_screen_geometry() if self.settings.get_property('render-on-screen') == "default-monitor": screen = get_primary_screen_geometry() # The topmost pixel of the window should be at 1/5 of the current screen's height # Window should be positioned in the center horizontally # Also, add offset x and y, in order to move window to the current screen self.move(screen['width'] / 2 - window_width / 2 + screen['x'], screen['height'] / 5 + screen['y']) def show_window(self): # works only when the following methods are called in that exact order self.window.set_sensitive(True) self.window.present() self.position_window() if not is_wayland_compatibility_on(): self.present_with_time(Keybinder.get_current_event_time()) if not self.input.get_text(): # make sure frequent apps are shown if necessary self.show_results([]) elif self.settings.get_property('clear-previous-query'): self.input.set_text('') else: self.input.grab_focus() def toggle_window(self, key=None): if self.is_visible(): self.hide() else: self.show_window() def bind_show_app_hotkey(self, accel_name): if is_wayland_compatibility_on(): return if self._current_accel_name == accel_name: return if self._current_accel_name: Keybinder.unbind(self._current_accel_name) self._current_accel_name = None logger.info("Trying to bind app hotkey: %s", accel_name) Keybinder.bind(accel_name, self.toggle_window) self._current_accel_name = accel_name self.notify_hotkey_change(accel_name) def notify_hotkey_change(self, accel_name): (key, mode) = Gtk.accelerator_parse(accel_name) display_name = Gtk.accelerator_get_label(key, mode) app_cache_db = AppCacheDb.get_instance() if not app_cache_db.find('startup_hotkey_notification'): app_cache_db.put('startup_hotkey_notification', True) app_cache_db.commit() show_notification("Ulauncher", "Hotkey is set to %s" % display_name) def _get_user_query(self): return Query(self.input.get_text()) def select_result_item(self, index, onHover=False): if time.time() - self._results_render_time > 0.1: # Work around issue #23 -- don't automatically select item if cursor is hovering over it upon render self.results_nav.select(index) def enter_result_item(self, index=None, alt=False): if not self.results_nav.enter(self._get_user_query(), index, alt=alt): # hide the window if it has to be closed on enter self.hide_and_clear_input() def hide(self, *args, **kwargs): """Override the hide method to ensure the pointer grab is released.""" if self.settings.get_property('grab-mouse-pointer'): self.get_pointer_device().ungrab(0) super(UlauncherWindow, self).hide(*args, **kwargs) def get_pointer_device(self): return (self .window .get_window() .get_display() .get_device_manager() .get_client_pointer()) def hide_and_clear_input(self): self.input.set_text('') self.hide() def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) show_recent_apps = self.settings.get_property('show-recent-apps') recent_apps_number = 3 if show_recent_apps else 0 try: recent_apps_number = int(str(show_recent_apps)) except ValueError: logger.warning("show-recent-apps in settings is not a number, fallback do default value") if not result_items and not self.input.get_text() and recent_apps_number > 0: result_items = AppStatDb.get_instance().get_most_frequent(recent_apps_number) results = self.create_item_widgets(result_items, self._get_user_query()) if results: self._results_render_time = time.time() for item in results: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) logger.debug('render %s results', len(results)) def _render_prefs_icon(self): scale_factor = get_monitor_scale_factor() prefs_pixbuf = load_image(get_data_file('media', 'gear.svg'), 16 * scale_factor) surface = Gdk.cairo_surface_create_from_pixbuf(prefs_pixbuf, scale_factor, self.get_window()) prefs_image = Gtk.Image.new_from_surface(surface) self.prefs_btn.set_image(prefs_image) @staticmethod def create_item_widgets(items, query): results = [] for index, result_item in enumerate(items): glade_filename = get_data_file('ui', '%s.ui' % result_item.UI_FILE) if not os.path.exists(glade_filename): glade_filename = None builder = Gtk.Builder() builder.set_translation_domain('ulauncher') builder.add_from_file(glade_filename) item_frame = builder.get_object('item-frame') item_frame.initialize(builder, result_item, index, query) results.append(item_frame) return results
def nav(self, items): return ItemNavigation(items)
class UlauncherWindow(Gtk.ApplicationWindow): __gtype_name__ = "UlauncherWindow" input: Gtk.Entry # These have to be declared on a separate line for some reason prefs_btn: Gtk.Button result_box: Gtk.Box scroll_container: Gtk.ScrolledWindow window_body: Gtk.Box input = Gtk.Template.Child("input") prefs_btn = Gtk.Template.Child("prefs_btn") result_box = Gtk.Template.Child("result_box") scroll_container = Gtk.Template.Child("result_box_scroll_container") window_body = Gtk.Template.Child("body") results_nav = None settings = Settings.get_instance() is_focused = False initial_query = None _css_provider = None _drag_start_coords = None @classmethod @singleton def get_instance(cls): return cls() ###################################### # GTK Signal Handlers ###################################### @Gtk.Template.Callback() def on_focus_out(self, widget, event): # apparently Gtk doesn't provide a mechanism to tell if window is in focus # this is a simple workaround to avoid hiding window # when user hits Alt+key combination or changes input source, etc. self.is_focused = False timer(0.07, lambda: self.is_focused or self.hide()) @Gtk.Template.Callback() def on_focus_in(self, *args): if self.settings.get_property('grab-mouse-pointer'): ptr_dev = self.get_pointer_device() result = ptr_dev.grab(self.get_window(), Gdk.GrabOwnership.NONE, True, Gdk.EventMask.ALL_EVENTS_MASK, None, 0) logger.debug("Focus in event, grabbing pointer: %s", result) self.is_focused = True @Gtk.Template.Callback() def on_input_changed(self, entry): """ Triggered by user input """ query = self._get_user_query() # This might seem odd, but this makes sure any normalization done in get_user_query() is # reflected in the input box. In particular, stripping out the leading white-space. self.input.set_text(query) ModeHandler.get_instance().on_query_change(query) @Gtk.Template.Callback() def on_input_key_press(self, widget, event): keyval = event.get_keyval() keyname = Gdk.keyval_name(keyval[1]) alt = event.state & Gdk.ModifierType.MOD1_MASK ctrl = event.state & Gdk.ModifierType.CONTROL_MASK jump_keys = self.settings.get_jump_keys() ModeHandler.get_instance().on_key_press_event(widget, event, self._get_user_query()) if keyname == 'Escape': self.hide() elif ctrl and keyname == 'comma': self.show_preferences() elif self.results_nav: if keyname in ('Up', 'ISO_Left_Tab') or (ctrl and keyname == 'p'): self.results_nav.go_up() return True if keyname in ('Down', 'Tab') or (ctrl and keyname == 'n'): self.results_nav.go_down() return True if alt and keyname in ('Return', 'KP_Enter'): self.enter_result(alt=True) elif keyname in ('Return', 'KP_Enter'): self.enter_result() elif alt and keyname in jump_keys: # on Alt+<num/letter> try: self.select_result(jump_keys.index(keyname)) except IndexError: # selected non-existing result item pass return False ###################################### # Helpers ###################################### def get_input(self): return self.input def init_styles(self, path): if not self._css_provider: self._css_provider = Gtk.CssProvider() self._css_provider.load_from_path(path) self.apply_css(self) # pylint: disable=no-member visual = self.get_screen().get_rgba_visual() if visual: self.set_visual(visual) def apply_css(self, widget): Gtk.StyleContext.add_provider(widget.get_style_context(), self._css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) if isinstance(widget, Gtk.Container): widget.forall(self.apply_css) def set_cursor(self, cursor_name): # pylint: disable=no-member window_ = self.get_window() cursor = Gdk.Cursor.new_from_name(window_.get_display(), cursor_name) window_.set_cursor(cursor) def init_theme(self): load_available_themes() theme = Theme.get_current() theme.clear_cache() if self.settings.get_property('disable-window-shadow'): self.window_body.get_style_context().add_class('no-window-shadow') self._render_prefs_icon() self.init_styles(theme.compile_css()) @Gtk.Template.Callback() def show_preferences(self, *_): self.get_application().show_preferences() def position_window(self): monitor = get_monitor( self.settings.get_property('render-on-screen') != "default-monitor" ) geo = monitor.get_geometry() max_height = geo.height - ( geo.height * 0.15) - 100 # 100 is roughly the height of the text input window_width = 500 * get_scaling_factor() self.set_property('width-request', window_width) self.scroll_container.set_property('max-content-height', max_height) self.move(geo.width * 0.5 - window_width * 0.5 + geo.x, geo.y + geo.height * 0.12) def show_window(self): # works only when the following methods are called in that exact order self.present() self.position_window() if IS_X11_COMPATIBLE: self.present_with_time(Keybinder.get_current_event_time()) if self.initial_query: self.input.set_text(self.initial_query) self.input.set_position(len(self.initial_query)) self.initial_query = None elif not self._get_input_text(): # make sure frequent apps are shown if necessary self.show_results([]) elif self.settings.get_property('clear-previous-query'): self.input.set_text('') else: self.input.grab_focus() @Gtk.Template.Callback() def on_mouse_down(self, _, event): """ Prepare moving the window if the user drags """ # Only on left clicks and not on the results if event.button == 1 and event.y < 100: self.set_cursor("grab") self._drag_start_coords = {'x': event.x, 'y': event.y} @Gtk.Template.Callback() def on_mouse_up(self, *_): """ Clear drag to move event data """ self._drag_start_coords = None self.set_cursor("default") @Gtk.Template.Callback() def on_mouse_move(self, _, event): """ Move window if cursor is held """ start = self._drag_start_coords if start and event.state == Gdk.ModifierType.BUTTON1_MASK: self.move(event.x_root - start['x'], event.y_root - start['y']) def _get_input_text(self): return self.input.get_text().lstrip() def _get_user_query(self): return Query(self._get_input_text()) def select_result(self, index): self.results_nav.select(index) def enter_result(self, index=None, alt=False): if self.results_nav.enter(self._get_user_query(), index, alt=alt): # hide the window if it has to be closed on enter self.hide_and_clear_input() def hide(self, *args, **kwargs): """Override the hide method to ensure the pointer grab is released.""" if self.settings.get_property('grab-mouse-pointer'): self.get_pointer_device().ungrab(0) super().hide(*args, **kwargs) def get_pointer_device(self): return (self.get_window().get_display().get_device_manager(). get_client_pointer()) def hide_and_clear_input(self): self.input.set_text('') self.hide() def show_results(self, results): """ :param list results: list of Result instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) limit = len(self.settings.get_jump_keys()) or 25 show_recent_apps = self.settings.get_property('show-recent-apps') recent_apps_number = int( show_recent_apps) if show_recent_apps.isnumeric() else 0 if not self.input.get_text() and recent_apps_number > 0: results = AppResult.get_most_frequent(recent_apps_number) results = self.create_item_widgets(results, self._get_user_query()) if results: for item in results[:limit]: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) self.scroll_container.show_all() else: # Hide the scroll container when there are no results since it normally takes up a # minimum amount of space even if it is empty. self.scroll_container.hide() logger.debug('render %s results', len(results)) def _render_prefs_icon(self): prefs_pixbuf = load_icon(get_asset('icons/gear.svg'), 16 * get_scaling_factor()) prefs_image = Gtk.Image.new_from_pixbuf(prefs_pixbuf) self.prefs_btn.set_image(prefs_image) @staticmethod def create_item_widgets(items, query): results = [] for index, result in enumerate(items): glade_filename = get_asset(f"ui/{result.UI_FILE}.ui") if not os.path.exists(glade_filename): glade_filename = None builder = Gtk.Builder() builder.set_translation_domain('ulauncher') builder.add_from_file(glade_filename) item_frame = builder.get_object('item-frame') item_frame.initialize(builder, result, index, query) results.append(item_frame) return results
class UlauncherWindow(WindowBase): __gtype_name__ = "UlauncherWindow" _current_accel_name = None _resultsRenderTime = 0 _mainWindowWasActivated = False @classmethod @singleton def get_instance(cls): return cls() def get_widget(self, id): """ Return widget instance by its ID """ return self.builder.get_object(id) def finish_initializing(self, builder): """ Set up the main window """ super(UlauncherWindow, self).finish_initializing(builder) self.results_nav = None self.builder = builder self.window = self.get_widget('ulauncher_window') self.input = self.get_widget('input') self.prefs_btn = self.get_widget('prefs_btn') self.result_box = builder.get_object("result_box") self.input.connect('changed', self.on_input_changed) self.prefs_btn.connect('clicked', self.on_mnu_preferences_activate) self.set_keep_above(True) self.AboutDialog = AboutUlauncherDialog self.PreferencesDialog = PreferencesUlauncherDialog self.position_window() self.init_styles() # bind hotkey Keybinder.init() accel_name = Settings.get_instance().get_property('hotkey-show-app') # bind in the main thread GLib.idle_add(self.bind_show_app_hotkey, accel_name) start_app_watcher() def position_window(self): window_width = self.get_size()[0] current_screen = get_current_screen_geometry() # The topmost pixel of the window should be at 1/5 of the current screen's height # Window should be positioned in the center horizontally # Also, add offset x and y, in order to move window to the current screen self.move(current_screen['width'] / 2 - window_width / 2 + current_screen['x'], current_screen['height'] / 5 + current_screen['y']) def init_styles(self): self.provider = Gtk.CssProvider() self.provider.load_from_path(get_data_file('ui', 'ulauncher.css')) self.apply_css(self, self.provider) self.screen = self.get_screen() self.visual = self.screen.get_rgba_visual() if self.visual is not None and self.screen.is_composited(): self.set_visual(self.visual) def apply_css(self, widget, provider): Gtk.StyleContext.add_provider(widget.get_style_context(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) if isinstance(widget, Gtk.Container): widget.forall(self.apply_css, provider) def on_focus_out_event(self, widget, event): # apparently Gtk doesn't provide a mechanism to tell if window is in focus # this is a simple workaround to avoid hiding window # when user hits Alt+key combination or changes input source, etc. self.is_focused = False t = threading.Timer(0.07, lambda: self.is_focused or self.hide()) t.start() def on_focus_in_event(self, *args): self.is_focused = True def show_window(self): self._mainWindowWasActivated = True # works only when the following methods are called in that exact order self.input.set_text('') self.position_window() self.window.set_sensitive(True) self.window.present() self.present_with_time(Keybinder.get_current_event_time()) def cb_toggle_visibility(self, key): self.hide() if self.is_visible() else self.show_window() def bind_show_app_hotkey(self, accel_name): if self._current_accel_name == accel_name: return if self._current_accel_name: Keybinder.unbind(self._current_accel_name) self._current_accel_name = None logger.info("Trying to bind app hotkey: %s" % accel_name) Keybinder.bind(accel_name, self.cb_toggle_visibility) self._current_accel_name = accel_name def get_user_query(self): return Query(self.input.get_text()) def on_input_changed(self, entry): """ Triggered by user input """ Search.get_instance().start(self.get_user_query()) def select_result_item(self, index, onHover=False): if time.time() - self._resultsRenderTime > 0.1: # Work around issue #23 -- don't automatically select item if cursor is hovering over it upon render self.results_nav.select(index) def enter_result_item(self, index=None, alt=False): if not self.results_nav.enter(self.get_user_query(), index, alt=alt): # close the window if it has to be closed on enter self.hide() def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) results = list(create_item_widgets(result_items, self.get_user_query())) # generator -> list if results: self._resultsRenderTime = time.time() map(self.result_box.add, results) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self.get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box, self.provider) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) def on_input_key_press_event(self, widget, event): keyval = event.get_keyval() keyname = Gdk.keyval_name(keyval[1]) alt = event.state & Gdk.ModifierType.MOD1_MASK Search.get_instance().on_key_press_event(widget, event, self.get_user_query()) if self.results_nav: if keyname == 'Up': self.results_nav.go_up() elif keyname == 'Down': self.results_nav.go_down() elif alt and keyname in ('Return', 'KP_Enter'): self.enter_result_item(alt=True) elif keyname in ('Return', 'KP_Enter'): self.enter_result_item() elif alt and keyname.isdigit() and 0 < int(keyname) < 10: # on Alt+<num> try: self.enter_result_item(int(keyname) - 1) except IndexError: # selected non-existing result item pass elif alt and len(keyname) == 1 and 97 <= ord(keyname) <= 122: # on Alt+<char> try: self.enter_result_item(ord(keyname) - 97 + 9) except IndexError: # selected non-existing result item pass if keyname == 'Escape': self.hide()
class UlauncherWindow(Gtk.Window, WindowHelper): __gtype_name__ = "UlauncherWindow" _current_accel_name = None _results_render_time = 0 @classmethod @singleton def get_instance(cls): return cls() def __new__(cls): """Special static method that's automatically called by Python when constructing a new instance of this class. Returns a fully instantiated BaseUlauncherWindow object. """ builder = Builder.new_from_file('UlauncherWindow') new_object = builder.get_object("ulauncher_window") new_object.finish_initializing(builder) return new_object def finish_initializing(self, builder): """Called while initializing this instance in __new__ finish_initializing should be called after parsing the UI definition and creating a UlauncherWindow object with it in order to finish initializing the start of the new UlauncherWindow instance. """ # Get a reference to the builder and set up the signals. self.builder = builder self.ui = builder.get_ui(self, True) self.PreferencesDialog = None # class self.preferences_dialog = None # instance self.results_nav = None self.window = self.ui['ulauncher_window'] self.input = self.ui['input'] self.prefs_btn = self.ui['prefs_btn'] self.result_box = self.ui["result_box"] self.input.connect('changed', self.on_input_changed) self.prefs_btn.connect('clicked', self.on_mnu_preferences_activate) self.set_keep_above(True) self.PreferencesDialog = PreferencesUlauncherDialog self.settings = Settings.get_instance() self.fix_window_width() self.position_window() self.init_theme() # this will trigger to show frequent apps if necessary self.show_results([]) if not is_wayland_compatibility_on(): # bind hotkey Keybinder.init() accel_name = self.settings.get_property('hotkey-show-app') # bind in the main thread GLib.idle_add(self.bind_show_app_hotkey, accel_name) start_app_watcher() ExtensionServer.get_instance().start() time.sleep(0.01) ExtensionRunner.get_instance().run_all() ExtensionDownloader.get_instance().download_missing() ###################################### # GTK Signal Handlers ###################################### def on_mnu_about_activate(self, widget, data=None): """Display the about page for ulauncher.""" self.activate_preferences(page='about') def on_mnu_preferences_activate(self, widget, data=None): """Display the preferences window for ulauncher.""" self.activate_preferences(page='preferences') def on_mnu_close_activate(self, widget, data=None): """Signal handler for closing the UlauncherWindow.""" self.destroy() def on_destroy(self, widget, data=None): """Called when the UlauncherWindow is closed.""" # Clean up code for saving application state should be added here. Gtk.main_quit() def on_preferences_dialog_destroyed(self, widget, data=None): '''only affects GUI logically there is no difference between the user closing, minimizing or ignoring the preferences dialog''' logger.debug('on_preferences_dialog_destroyed') # to determine whether to create or present preferences_dialog self.preferences_dialog = None def on_focus_out_event(self, widget, event): # apparently Gtk doesn't provide a mechanism to tell if window is in focus # this is a simple workaround to avoid hiding window # when user hits Alt+key combination or changes input source, etc. self.is_focused = False t = threading.Timer(0.07, lambda: self.is_focused or self.hide()) t.start() def on_focus_in_event(self, *args): self.is_focused = True def on_input_changed(self, entry): """ Triggered by user input """ Search.get_instance().on_query_change(self._get_user_query()) def on_input_key_press_event(self, widget, event): keyval = event.get_keyval() keyname = Gdk.keyval_name(keyval[1]) alt = event.state & Gdk.ModifierType.MOD1_MASK Search.get_instance().on_key_press_event(widget, event, self._get_user_query()) if self.results_nav: if keyname == 'Up': self.results_nav.go_up() elif keyname == 'Down': self.results_nav.go_down() elif alt and keyname in ('Return', 'KP_Enter'): self.enter_result_item(alt=True) elif keyname in ('Return', 'KP_Enter'): self.enter_result_item() elif alt and keyname.isdigit() and 0 < int(keyname) < 10: # on Alt+<num> try: self.enter_result_item(int(keyname) - 1) except IndexError: # selected non-existing result item pass elif alt and len(keyname) == 1 and 97 <= ord(keyname) <= 122: # on Alt+<char> try: self.enter_result_item(ord(keyname) - 97 + 9) except IndexError: # selected non-existing result item pass if keyname == 'Escape': self.hide() ###################################### # Helpers ###################################### def get_input(self): return self.input def fix_window_width(self): """ Add 2px to the window width if GTK+ >= 3.20 Because of the bug in <3.20 that doesn't add css borders to the width """ if gtk_version_is_gte(3, 20, 0): width, height = self.get_size_request() self.set_size_request(width + 2, height) def init_theme(self): load_available_themes() theme = Theme.get_current() theme.clear_cache() self.init_styles(theme.compile_css()) def activate_preferences(self, page='preferences'): """ From the PyGTK Reference manual Say for example the preferences dialog is currently open, and the user chooses Preferences from the menu a second time; use the present() method to move the already-open dialog where the user can see it. """ self.hide() if self.preferences_dialog is not None: logger.debug('show existing preferences_dialog') self.preferences_dialog.present(page=page) elif self.PreferencesDialog is not None: logger.debug('create new preferences_dialog') self.preferences_dialog = self.PreferencesDialog() # pylint: disable=E1102 self.preferences_dialog.connect('destroy', self.on_preferences_dialog_destroyed) self.preferences_dialog.show(page=page) # destroy command moved into dialog to allow for a help button def position_window(self): window_width = self.get_size()[0] current_screen = get_current_screen_geometry() # The topmost pixel of the window should be at 1/5 of the current screen's height # Window should be positioned in the center horizontally # Also, add offset x and y, in order to move window to the current screen self.move(current_screen['width'] / 2 - window_width / 2 + current_screen['x'], current_screen['height'] / 5 + current_screen['y']) def show_window(self): # works only when the following methods are called in that exact order self.position_window() self.window.set_sensitive(True) self.window.present() if not is_wayland_compatibility_on(): self.present_with_time(Keybinder.get_current_event_time()) if not self.input.get_text(): # make sure frequent apps are shown if necessary self.show_results([]) elif self.settings.get_property('clear-previous-query'): self.input.set_text('') else: self.input.grab_focus() def toggle_window(self, key=None): self.hide() if self.is_visible() else self.show_window() def bind_show_app_hotkey(self, accel_name): if is_wayland_compatibility_on(): return if self._current_accel_name == accel_name: return if self._current_accel_name: Keybinder.unbind(self._current_accel_name) self._current_accel_name = None logger.info("Trying to bind app hotkey: %s" % accel_name) Keybinder.bind(accel_name, self.toggle_window) self._current_accel_name = accel_name self.notify_hotkey_change(accel_name) def notify_hotkey_change(self, accel_name): (key, mode) = Gtk.accelerator_parse(accel_name) display_name = Gtk.accelerator_get_label(key, mode) app_cache_db = AppCacheDb.get_instance() if not app_cache_db.find('startup_hotkey_notification'): show_notification("Ulauncher", "Hotkey is set to %s" % display_name) app_cache_db.put('startup_hotkey_notification', True) app_cache_db.commit() def _get_user_query(self): # get_text() returns str, so we need to convert it to unicode return Query(force_unicode(self.input.get_text())) def select_result_item(self, index, onHover=False): if time.time() - self._results_render_time > 0.1: # Work around issue #23 -- don't automatically select item if cursor is hovering over it upon render self.results_nav.select(index) def enter_result_item(self, index=None, alt=False): if not self.results_nav.enter(self._get_user_query(), index, alt=alt): # hide the window if it has to be closed on enter self.hide_and_clear_input() def hide_and_clear_input(self): self.input.set_text('') self.hide() def show_results(self, result_items): """ :param list result_items: list of ResultItem instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) if not result_items and not self.input.get_text() and self.settings.get_property('show-recent-apps'): result_items = AppStatDb.get_instance().get_most_frequent(3) results = self.create_item_widgets(result_items, self._get_user_query()) if results: self._results_render_time = time.time() map(self.result_box.add, results) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.show_all() self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) else: self.result_box.set_margin_bottom(0) self.result_box.set_margin_top(0) logger.debug('render %s results' % len(results)) @staticmethod def create_item_widgets(items, query): results = [] for index, result_item in enumerate(items): glade_filename = get_data_file('ui', '%s.ui' % result_item.UI_FILE) if not os.path.exists(glade_filename): glade_filename = None builder = Gtk.Builder() builder.set_translation_domain('ulauncher') builder.add_from_file(glade_filename) item_frame = builder.get_object('item-frame') item_frame.initialize(builder, result_item, index, query) results.append(item_frame) return results
class UlauncherWindow(Gtk.ApplicationWindow): __gtype_name__ = "UlauncherWindow" _current_accel_name = None _results_render_time = 0 _css_provider = None _drag_start_coords = None @classmethod @singleton def get_instance(cls): return cls() # Python's GTK API seems to requires non-standard workarounds like this. # Use finish_initializing instead of __init__. def __new__(cls): return GladeObjectFactory(cls.__name__, "ulauncher_window") def finish_initializing(self, ui): # pylint: disable=attribute-defined-outside-init self.ui = ui self.preferences = None # instance self.results_nav = None self.window_body = self.ui['body'] self.input = self.ui['input'] self.prefs_btn = self.ui['prefs_btn'] self.result_box = self.ui["result_box"] self.scroll_container = self.ui["result_box_scroll_container"] self.input.connect('changed', self.on_input_changed) self.prefs_btn.connect('clicked', self.on_mnu_preferences_activate) self.set_keep_above(True) self.settings = Settings.get_instance() self.fix_window_width() self.position_window() self.init_theme() # this will trigger to show frequent apps if necessary self.show_results([]) self.connect('button-press-event', self.mouse_down_event) self.connect('button-release-event', self.mouse_up_event) self.connect('motion_notify_event', self.mouse_move_event) if self.settings.get_property('show-indicator-icon'): AppIndicator.get_instance(self).show() if IS_X11: # bind hotkey Keybinder.init() accel_name = self.settings.get_property('hotkey-show-app') # bind in the main thread GLib.idle_add(self.bind_hotkey, accel_name) ExtensionServer.get_instance().start() time.sleep(0.01) ExtensionRunner.get_instance().run_all() if not get_options().no_extensions: ExtensionDownloader.get_instance().download_missing() ###################################### # GTK Signal Handlers ###################################### # pylint: disable=unused-argument def on_mnu_about_activate(self, widget, data=None): """Display the about page for ulauncher.""" self.activate_preferences(page='about') def on_mnu_preferences_activate(self, widget, data=None): """Display the preferences window for ulauncher.""" self.activate_preferences(page='preferences') def on_preferences_destroyed(self, widget, data=None): '''only affects GUI logically there is no difference between the user closing, minimizing or ignoring the preferences dialog''' logger.debug('on_preferences_destroyed') # to determine whether to create or present preferences self.preferences = None def on_focus_out_event(self, widget, event): # apparently Gtk doesn't provide a mechanism to tell if window is in focus # this is a simple workaround to avoid hiding window # when user hits Alt+key combination or changes input source, etc. self.is_focused = False timer(0.07, lambda: self.is_focused or self.hide()) def on_focus_in_event(self, *args): if self.settings.get_property('grab-mouse-pointer'): ptr_dev = self.get_pointer_device() result = ptr_dev.grab(self.get_window(), Gdk.GrabOwnership.NONE, True, Gdk.EventMask.ALL_EVENTS_MASK, None, 0) logger.debug("Focus in event, grabbing pointer: %s", result) self.is_focused = True def on_input_changed(self, entry): """ Triggered by user input """ query = self._get_user_query() # This might seem odd, but this makes sure any normalization done in get_user_query() is # reflected in the input box. In particular, stripping out the leading white-space. self.input.set_text(query) ModeHandler.get_instance().on_query_change(query) # pylint: disable=inconsistent-return-statements def on_input_key_press_event(self, widget, event): keyval = event.get_keyval() keyname = Gdk.keyval_name(keyval[1]) alt = event.state & Gdk.ModifierType.MOD1_MASK ctrl = event.state & Gdk.ModifierType.CONTROL_MASK jump_keys = self.settings.get_jump_keys() ModeHandler.get_instance().on_key_press_event(widget, event, self._get_user_query()) if keyname == 'Escape': self.hide() elif ctrl and keyname == 'comma': self.activate_preferences() elif self.results_nav: if keyname in ('Up', 'ISO_Left_Tab') or (ctrl and keyname == 'p'): self.results_nav.go_up() return True if keyname in ('Down', 'Tab') or (ctrl and keyname == 'n'): self.results_nav.go_down() return True if alt and keyname in ('Return', 'KP_Enter'): self.enter_result(alt=True) elif keyname in ('Return', 'KP_Enter'): self.enter_result() elif alt and keyname in jump_keys: # on Alt+<num/letter> try: self.select_result(jump_keys.index(keyname)) except IndexError: # selected non-existing result item pass ###################################### # Helpers ###################################### def get_input(self): return self.input def fix_window_width(self): """ Add 2px to the window width if GTK+ >= 3.20 Because of the bug in <3.20 that doesn't add css borders to the width """ width, height = self.get_size_request() self.set_size_request(width + 2, height) def init_styles(self, path): if not self._css_provider: self._css_provider = Gtk.CssProvider() self._css_provider.load_from_path(path) self.apply_css(self) # pylint: disable=no-member visual = self.get_screen().get_rgba_visual() if visual: self.set_visual(visual) def apply_css(self, widget): Gtk.StyleContext.add_provider(widget.get_style_context(), self._css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) if isinstance(widget, Gtk.Container): widget.forall(self.apply_css) def set_cursor(self, cursor_name): # pylint: disable=no-member window_ = self.get_window() cursor = Gdk.Cursor.new_from_name(window_.get_display(), cursor_name) window_.set_cursor(cursor) def init_theme(self): load_available_themes() theme = Theme.get_current() theme.clear_cache() if self.settings.get_property('disable-window-shadow'): self.window_body.get_style_context().add_class('no-window-shadow') self._render_prefs_icon() self.init_styles(theme.compile_css()) def activate_preferences(self, page='preferences'): self.hide() if self.preferences is not None: logger.debug('Show existing preferences window') self.preferences.present(page=page) else: logger.debug('Create new preferences window') self.preferences = PreferencesWindow() self.preferences.set_application(self.get_application()) self.preferences.connect('destroy', self.on_preferences_destroyed) self.preferences.show(page=page) # destroy command moved into dialog to allow for a help button def position_window(self): monitor = get_monitor( self.settings.get_property('render-on-screen') != "default-monitor" ) geo = monitor.get_geometry() max_height = geo.height - ( geo.height * 0.15) - 100 # 100 is roughly the height of the text input window_width = 500 * get_scaling_factor() self.set_property('width-request', window_width) self.ui["result_box_scroll_container"].set_property( 'max-content-height', max_height) self.move(geo.width * 0.5 - window_width * 0.5 + geo.x, geo.y + geo.height * 0.12) def show_window(self): # works only when the following methods are called in that exact order self.present() self.position_window() if IS_X11_COMPATIBLE: self.present_with_time(Keybinder.get_current_event_time()) if not self._get_input_text(): # make sure frequent apps are shown if necessary self.show_results([]) elif self.settings.get_property('clear-previous-query'): self.input.set_text('') else: self.input.grab_focus() def mouse_down_event(self, _, event): """ Prepare moving the window if the user drags """ # Only on left clicks and not on the results if event.button == 1 and event.y < 100: self.set_cursor("grab") self._drag_start_coords = {'x': event.x, 'y': event.y} def mouse_up_event(self, *_): """ Clear drag to move event data """ self._drag_start_coords = None self.set_cursor("default") def mouse_move_event(self, _, event): """ Move window if cursor is held """ start = self._drag_start_coords if start and event.state == Gdk.ModifierType.BUTTON1_MASK: self.move(event.x_root - start['x'], event.y_root - start['y']) def bind_hotkey(self, accel_name): if not IS_X11 or self._current_accel_name == accel_name: return if self._current_accel_name: Keybinder.unbind(self._current_accel_name) self._current_accel_name = None logger.info("Trying to bind app hotkey: %s", accel_name) Keybinder.bind(accel_name, self.show_window) self._current_accel_name = accel_name if FIRST_RUN: (key, mode) = Gtk.accelerator_parse(accel_name) display_name = Gtk.accelerator_get_label(key, mode) show_notification("Ulauncher", f"Hotkey is set to {display_name}") def _get_input_text(self): return self.input.get_text().lstrip() def _get_user_query(self): return Query(self._get_input_text()) def select_result(self, index, onHover=False): if time.time() - self._results_render_time > 0.1: # Work around issue #23 -- don't automatically select item if cursor is hovering over it upon render self.results_nav.select(index) def enter_result(self, index=None, alt=False): if self.results_nav.enter(self._get_user_query(), index, alt=alt): # hide the window if it has to be closed on enter self.hide_and_clear_input() def hide(self, *args, **kwargs): """Override the hide method to ensure the pointer grab is released.""" if self.settings.get_property('grab-mouse-pointer'): self.get_pointer_device().ungrab(0) super().hide(*args, **kwargs) def get_pointer_device(self): return (self.get_window().get_display().get_device_manager(). get_client_pointer()) def hide_and_clear_input(self): self.input.set_text('') self.hide() def show_results(self, results): """ :param list results: list of Result instances """ self.results_nav = None self.result_box.foreach(lambda w: w.destroy()) limit = len(self.settings.get_jump_keys()) or 25 show_recent_apps = self.settings.get_property('show-recent-apps') recent_apps_number = int( show_recent_apps) if show_recent_apps.isnumeric() else 0 if not self.input.get_text() and recent_apps_number > 0: results = AppResult.get_most_frequent(recent_apps_number) results = self.create_item_widgets(results, self._get_user_query()) if results: self._results_render_time = time.time() for item in results[:limit]: self.result_box.add(item) self.results_nav = ItemNavigation(self.result_box.get_children()) self.results_nav.select_default(self._get_user_query()) self.result_box.set_margin_bottom(10) self.result_box.set_margin_top(3) self.apply_css(self.result_box) self.scroll_container.show_all() else: # Hide the scroll container when there are no results since it normally takes up a # minimum amount of space even if it is empty. self.scroll_container.hide() logger.debug('render %s results', len(results)) def _render_prefs_icon(self): prefs_pixbuf = load_icon(get_asset('icons/gear.svg'), 16 * get_scaling_factor()) prefs_image = Gtk.Image.new_from_pixbuf(prefs_pixbuf) self.prefs_btn.set_image(prefs_image) @staticmethod def create_item_widgets(items, query): results = [] for index, result in enumerate(items): glade_filename = get_asset(f"ui/{result.UI_FILE}.ui") if not os.path.exists(glade_filename): glade_filename = None builder = Gtk.Builder() builder.set_translation_domain('ulauncher') builder.add_from_file(glade_filename) item_frame = builder.get_object('item-frame') item_frame.initialize(builder, result, index, query) results.append(item_frame) return results