Ejemplo n.º 1
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
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.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
Ejemplo n.º 4
0
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()
Ejemplo n.º 5
0
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