Ejemplo n.º 1
0
class CoreModel(GObject.GObject):
    """Provides all the list models used in Music

    Music is using a hierarchy of data objects with list models to
    contain the information about the users available music. This
    hierarchy is filled mainly through Grilo, with the exception of
    playlists which are a Tracker only feature.

    There are three main models: one for artist info, one for albums
    and one for songs. The data objects within these are CoreArtist,
    CoreAlbum and CoreSong respectively.

    The data objects contain filtered lists of the three main models.
    This makes the hierarchy as follows.

    CoreArtist -> CoreAlbum -> CoreDisc -> CoreSong

    Playlists are a Tracker only feature and do not use the three
    main models directly.

    GrlTrackerPlaylists -> Playlist -> CoreSong

    The Player playlist is a copy of the relevant playlist, built by
    using the components described above as needed.
    """

    __gsignals__ = {
        "activate-playlist":
        (GObject.SignalFlags.RUN_FIRST, None, (Playlist, )),
        "artists-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
        "playlist-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
        "playlists-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    grilo = GObject.Property(type=CoreGrilo, default=None)
    songs_available = GObject.Property(type=bool, default=False)

    def __init__(self, coreselection):
        super().__init__()

        self._flatten_model = None
        self._previous_playlist_model = None
        self._search_signal_id = None
        self._song_signal_id = None

        self._model = Gio.ListStore.new(CoreSong)
        self._songliststore = SongListStore(self._model)

        self._coreselection = coreselection
        self._album_model = Gio.ListStore()
        self._album_model_sort = Gfm.SortListModel.new(self._album_model)
        self._album_model_sort.set_sort_func(
            self._wrap_list_store_sort_func(self._albums_sort))

        self._artist_model = Gio.ListStore.new(CoreArtist)
        self._artist_model_sort = Gfm.SortListModel.new(self._artist_model)
        self._artist_model_sort.set_sort_func(
            self._wrap_list_store_sort_func(self._artist_sort))

        self._playlist_model = Gio.ListStore.new(CoreSong)
        self._playlist_model_sort = Gfm.SortListModel.new(self._playlist_model)

        self._song_search_proxy = Gio.ListStore.new(Gfm.FilterListModel)
        self._song_search_flatten = Gfm.FlattenListModel.new(CoreSong)
        self._song_search_flatten.set_model(self._song_search_proxy)

        self._album_search_model = Dazzle.ListModelFilter.new(
            self._album_model)
        self._album_search_model.set_filter_func(lambda a: False)

        self._album_search_filter = Gfm.FilterListModel.new(
            self._album_search_model)

        self._artist_search_model = Dazzle.ListModelFilter.new(
            self._artist_model)
        self._artist_search_model.set_filter_func(lambda a: False)

        self._artist_search_filter = Gfm.FilterListModel.new(
            self._artist_search_model)

        self._playlists_model = Gio.ListStore.new(Playlist)
        self._playlists_model_filter = Dazzle.ListModelFilter.new(
            self._playlists_model)
        self._playlists_model_sort = Gfm.SortListModel.new(
            self._playlists_model_filter)
        self._playlists_model_sort.set_sort_func(
            self._wrap_list_store_sort_func(self._playlists_sort))

        self.props.grilo = CoreGrilo(self, self._coreselection)
        # FIXME: Not all instances of internal _grilo use have been
        # fixed.
        self._grilo = self.props.grilo

        self._model.connect("items-changed", self._on_songs_items_changed)

    def _on_songs_items_changed(self, model, position, removed, added):
        available = self.props.songs_available
        now_available = model.get_n_items() > 0

        if available == now_available:
            return

        if model.get_n_items() > 0:
            self.props.songs_available = True
        else:
            self.props.songs_available = False

    def _filter_selected(self, coresong):
        return coresong.props.selected

    def _albums_sort(self, album_a, album_b):
        return album_b.props.title.casefold() < album_a.props.title.casefold()

    def _artist_sort(self, artist_a, artist_b):
        name_a = artist_a.props.artist.casefold()
        name_b = artist_b.props.artist.casefold()
        return name_a > name_b

    def _playlists_sort(self, playlist_a, playlist_b):
        if playlist_a.props.is_smart:
            if not playlist_b.props.is_smart:
                return -1
            title_a = playlist_a.props.title.casefold()
            title_b = playlist_b.props.title.casefold()
            return title_a > title_b

        if playlist_b.props.is_smart:
            return 1

        # cannot use GLib.DateTime.compare
        # https://gitlab.gnome.org/GNOME/pygobject/issues/334
        # newest first
        date_diff = playlist_b.props.creation_date.difference(
            playlist_a.props.creation_date)
        return math.copysign(1, date_diff)

    def _wrap_list_store_sort_func(self, func):
        def wrap(a, b, *user_data):
            a = pygobject_new_full(a, False)
            b = pygobject_new_full(b, False)
            return func(a, b, *user_data)

        return wrap

    def get_album_model(self, media):
        disc_model = Gio.ListStore()
        disc_model_sort = Gfm.SortListModel.new(disc_model)

        def _disc_order_sort(disc_a, disc_b):
            return disc_a.props.disc_nr - disc_b.props.disc_nr

        disc_model_sort.set_sort_func(
            self._wrap_list_store_sort_func(_disc_order_sort))

        self.props.grilo.get_album_discs(media, disc_model)

        return disc_model_sort

    def get_artist_album_model(self, media):
        albums_model_filter = Dazzle.ListModelFilter.new(self._album_model)
        albums_model_filter.set_filter_func(lambda a: False)

        albums_model_sort = Gfm.SortListModel.new(albums_model_filter)

        self.props.grilo.get_artist_albums(media, albums_model_filter)

        def _album_sort(album_a, album_b):
            return album_a.props.year > album_b.props.year

        albums_model_sort.set_sort_func(
            self._wrap_list_store_sort_func(_album_sort))

        return albums_model_sort

    def set_player_model(self, playlist_type, model):
        if model is self._previous_playlist_model:
            for song in self._playlist_model:
                if song.props.state == SongWidget.State.PLAYING:
                    song.props.state = SongWidget.State.PLAYED

            self.emit("playlist-loaded")
            return

        def _on_items_changed(model, position, removed, added):
            if removed > 0:
                for i in list(range(removed)):
                    self._playlist_model.remove(position)

            if added > 0:
                for i in list(range(added)):
                    coresong = model[position + i]
                    song = CoreSong(coresong.props.media, self._coreselection,
                                    self.props.grilo)

                    self._playlist_model.insert(position + i, song)

                    song.bind_property("state", coresong, "state",
                                       GObject.BindingFlags.SYNC_CREATE)
                    coresong.bind_property(
                        "validation", song, "validation",
                        GObject.BindingFlags.BIDIRECTIONAL
                        | GObject.BindingFlags.SYNC_CREATE)

        with model.freeze_notify():
            self._playlist_model.remove_all()

            if playlist_type == PlayerPlaylist.Type.ALBUM:
                proxy_model = Gio.ListStore.new(Gio.ListModel)

                for disc in model:
                    proxy_model.append(disc.props.model)

                self._flatten_model = Gfm.FlattenListModel.new(
                    CoreSong, proxy_model)
                self._flatten_model.connect("items-changed", _on_items_changed)

                for model_song in self._flatten_model:
                    song = CoreSong(model_song.props.media,
                                    self._coreselection, self.props.grilo)

                    self._playlist_model.append(song)
                    song.bind_property("state", model_song, "state",
                                       GObject.BindingFlags.SYNC_CREATE)
                    model_song.bind_property(
                        "validation", song, "validation",
                        GObject.BindingFlags.BIDIRECTIONAL
                        | GObject.BindingFlags.SYNC_CREATE)

                self.emit("playlist-loaded")
            elif playlist_type == PlayerPlaylist.Type.ARTIST:
                proxy_model = Gio.ListStore.new(Gio.ListModel)

                for artist_album in model:
                    for disc in artist_album.model:
                        proxy_model.append(disc.props.model)

                self._flatten_model = Gfm.FlattenListModel.new(
                    CoreSong, proxy_model)
                self._flatten_model.connect("items-changed", _on_items_changed)

                for model_song in self._flatten_model:
                    song = CoreSong(model_song.props.media,
                                    self._coreselection, self.props.grilo)

                    self._playlist_model.append(song)
                    song.bind_property("state", model_song, "state",
                                       GObject.BindingFlags.SYNC_CREATE)
                    model_song.bind_property(
                        "validation", song, "validation",
                        GObject.BindingFlags.BIDIRECTIONAL
                        | GObject.BindingFlags.SYNC_CREATE)

                self.emit("playlist-loaded")
            elif playlist_type == PlayerPlaylist.Type.SONGS:
                if self._song_signal_id:
                    self._songliststore.props.model.disconnect(
                        self._song_signal_id)

                for song in self._songliststore.props.model:
                    self._playlist_model.append(song)

                    if song.props.state == SongWidget.State.PLAYING:
                        song.props.state = SongWidget.State.PLAYED

                self._song_signal_id = self._songliststore.props.model.connect(
                    "items-changed", _on_items_changed)

                self.emit("playlist-loaded")
            elif playlist_type == PlayerPlaylist.Type.SEARCH_RESULT:
                if self._search_signal_id:
                    self._song_search_flatten.disconnect(
                        self._search_signal_id)

                for song in self._song_search_flatten:
                    self._playlist_model.append(song)

                self._search_signal_id = self._song_search_flatten.connect(
                    "items-changed", _on_items_changed)

                self.emit("playlist-loaded")
            elif playlist_type == PlayerPlaylist.Type.PLAYLIST:
                for model_song in model:
                    song = CoreSong(model_song.props.media,
                                    self._coreselection, self.props.grilo)

                    self._playlist_model.append(song)

                    song.bind_property("state", model_song, "state",
                                       GObject.BindingFlags.SYNC_CREATE)
                    model_song.bind_property(
                        "validation", song, "validation",
                        GObject.BindingFlags.BIDIRECTIONAL
                        | GObject.BindingFlags.SYNC_CREATE)

                self.emit("playlist-loaded")

        self._previous_playlist_model = model

    def stage_playlist_deletion(self, playlist):
        """Prepares playlist deletion.

        :param Playlist playlist: playlist
        """
        self.props.grilo.stage_playlist_deletion(playlist)

    def finish_playlist_deletion(self, playlist, deleted):
        """Finishes playlist deletion.

        :param Playlist playlist: playlist
        :param bool deleted: indicates if the playlist has been deleted
        """
        self.props.grilo.finish_playlist_deletion(playlist, deleted)

    def create_playlist(self, playlist_title, callback):
        """Creates a new user playlist.

        :param str playlist_title: playlist title
        :param callback: function to perform once, the playlist is created
        """
        self.props.grilo.create_playlist(playlist_title, callback)

    def activate_playlist(self, playlist):
        """Activates a playlist.

        Selects the playlist and start playing.

        :param Playlist playlist: playlist to activate
        """
        # FIXME: just a proxy
        self.emit("activate-playlist", playlist)

    def search(self, text):
        self.props.grilo.search(text)

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def songs(self):
        return self._model

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def albums(self):
        return self._album_model

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def artists(self):
        return self._artist_model

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def playlist(self):
        return self._playlist_model

    @GObject.Property(type=Gfm.SortListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def albums_sort(self):
        return self._album_model_sort

    @GObject.Property(type=Gfm.SortListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def artists_sort(self):
        return self._artist_model_sort

    @GObject.Property(type=Gfm.SortListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def playlist_sort(self):
        return self._playlist_model_sort

    @GObject.Property(type=Dazzle.ListModelFilter,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def songs_search(self):
        return self._song_search_flatten

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def songs_search_proxy(self):
        return self._song_search_proxy

    @GObject.Property(type=Dazzle.ListModelFilter,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def albums_search(self):
        return self._album_search_model

    @GObject.Property(type=Gfm.FilterListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def albums_search_filter(self):
        return self._album_search_filter

    @GObject.Property(type=Dazzle.ListModelFilter,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def artists_search(self):
        return self._artist_search_model

    @GObject.Property(type=Gfm.FilterListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def artists_search_filter(self):
        return self._artist_search_filter

    @GObject.Property(type=Gtk.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def songs_gtkliststore(self):
        return self._songliststore

    @GObject.Property(type=Gio.ListStore,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def playlists(self):
        return self._playlists_model

    @GObject.Property(type=Gfm.SortListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def playlists_sort(self):
        return self._playlists_model_sort

    @GObject.Property(type=Gfm.SortListModel,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def playlists_filter(self):
        return self._playlists_model_filter
Ejemplo n.º 2
0
class GSettingsStringComboBox(GSettingsComboBox):

    __gtype_name__ = "GSettingsStringComboBox"

    gsettings_column = GObject.Property(type=int, default=0)
    gsettings_value = GObject.Property(type=str, default="")
Ejemplo n.º 3
0
class Window(Gtk.ApplicationWindow, GObject.GObject):
    """Main Window object."""
    __gtype_name__ = 'Window'

    # Default Window instance
    instance = None

    view = GObject.Property(type=int, default=0)

    search_btn = Gtk.Template.Child()
    primary_menu_btn = Gtk.Template.Child()

    main_stack = Gtk.Template.Child()
    headerbar_stack = Gtk.Template.Child()
    accounts_stack = Gtk.Template.Child()

    search_bar = Gtk.Template.Child()
    password_entry = Gtk.Template.Child()

    def __init__(self):
        super(Window, self).__init__()
        self.init_template('Window')

        self.connect("notify::view", self.__state_changed)

        self.key_press_signal = None
        self.restore_state()
        # Start the Account Manager
        AccountsManager.get_default()

        self.__init_widgets()

    @staticmethod
    def get_default():
        """Return the default instance of Window."""
        if Window.instance is None:
            Window.instance = Window()
        return Window.instance

    def close(self):
        self.save_state()
        AccountsManager.get_default().kill()
        self.destroy()

    def add_account(self, *_):
        if not self.get_application().is_locked:
            add_window = AddAccountWindow()
            add_window.set_transient_for(self)
            add_window.set_size_request(*self.get_size())
            add_window.resize(*self.get_size())
            add_window.show_all()
            add_window.present()

    def set_menu(self, menu):
        popover = Gtk.Popover.new_from_model(self.primary_menu_btn, menu)

        def primary_menu_btn_handler(_, popover):
            popover.set_visible(not popover.get_visible())

        self.primary_menu_btn.connect('clicked', primary_menu_btn_handler,
                                      popover)

    def toggle_search(self, *_):
        """
            Switch the state of the search mode

            Switches the state of the search mode if:
                - The application is not locked
                - There are at least one account in the database
            return: None
        """
        if self.props.view == WindowView.NORMAL:
            toggled = not self.search_btn.props.active
            self.search_btn.set_property("active", toggled)

    def refresh_view(self, *_):
        if AccountsManager.get_default().props.empty:
            self.props.view = WindowView.EMPTY
        else:
            self.props.view = WindowView.NORMAL

    def save_state(self):
        """
            Save window position and maximized state.
        """
        settings = Settings.get_default()
        settings.window_position = self.get_position()
        settings.window_maximized = self.is_maximized()

    def restore_state(self):
        """
            Restore the window's state.
        """
        settings = Settings.get_default()
        # Restore the window position
        position_x, position_y = settings.window_position
        if position_x != 0 and position_y != 0:
            self.move(position_x, position_y)
            Logger.debug("[Window] Restore position x: {}, y: {}".format(
                position_x, position_y))
        else:
            # Fallback to the center
            self.set_position(Gtk.WindowPosition.CENTER)

        if settings.window_maximized:
            self.maximize()

    def __init_widgets(self):
        """Build main window widgets."""
        # Register Actions
        self.__add_action("add-account", self.add_account)
        self.__add_action("toggle-searchbar", self.toggle_search)

        # Set up accounts Widget
        accounts_widget = AccountsWidget.get_default()
        accounts_widget.connect("account-removed", self.refresh_view)
        accounts_widget.connect("account-added", self.refresh_view)
        self.accounts_stack.add_named(accounts_widget, "accounts")
        self.accounts_stack.set_visible_child_name("accounts")

        AccountsManager.get_default().connect("notify::empty",
                                              self.refresh_view)

        self.search_bar.bind_property("search-mode-enabled", self.search_btn,
                                      "active",
                                      GObject.BindingFlags.BIDIRECTIONAL)

    def __add_action(self,
                     key,
                     callback,
                     prop_bind=None,
                     bind_flag=GObject.BindingFlags.INVERT_BOOLEAN):
        action = Gio.SimpleAction.new(key, None)
        action.connect("activate", callback)
        if prop_bind:
            self.bind_property(prop_bind, action, "enabled", bind_flag)
        self.add_action(action)

    def __state_changed(self, *_):
        if self.props.view == WindowView.LOCKED:
            visible_child = "locked_state"
            visible_headerbar = "locked_headerbar"
            if self.key_press_signal:
                self.disconnect(self.key_press_signal)
        else:
            if self.props.view == WindowView.EMPTY:
                visible_child = "empty_state"
                visible_headerbar = "empty_headerbar"
            else:
                visible_child = "normal_state"
                visible_headerbar = "main_headerbar"
                # Connect on type search bar
                self.key_press_signal = self.connect(
                    "key-press-event",
                    lambda x, y: self.search_bar.handle_event(y))
        self.main_stack.set_visible_child_name(visible_child)
        self.headerbar_stack.set_visible_child_name(visible_headerbar)

    @Gtk.Template.Callback('unlock_btn_clicked')
    def __unlock_btn_clicked(self, *_):

        from Authenticator.models import Keyring
        typed_password = self.password_entry.get_text()
        if typed_password == Keyring.get_default().get_password():
            self.get_application().set_property("is-locked", False)
            # Reset password entry
            self.password_entry.get_style_context().remove_class("error")
            self.password_entry.set_text("")
        else:
            self.password_entry.get_style_context().add_class("error")

    @Gtk.Template.Callback('search_changed')
    def __search_changed(self, entry):
        """
            Handles search-changed signal.
        """
        def filter_func(row, data, *_):
            """
                Filter function
            """
            data = data.lower()
            if len(data) > 0:
                username = row.account.username.lower()
                provider_name = row.account.provider.name.lower()
                return (data in username or data in provider_name)
            else:
                return True

        data = entry.get_text().strip()
        search_lists = AccountsWidget.get_default().accounts_lists
        results_count = 0
        for search_list in search_lists:
            search_list.set_filter_func(filter_func, data, False)
            for elem in search_list:

                if elem.get_child_visible():
                    results_count += 1

        if results_count == 0:
            self.accounts_stack.set_visible_child_name("empty_results")
        else:
            self.accounts_stack.set_visible_child_name("accounts")
Ejemplo n.º 4
0
class DiscListBox(Gtk.Box):
    """A ListBox widget containing all discs of a particular
    album
    """
    __gtype_name__ = 'DiscListBox'

    __gsignals__ = {
        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    selection_mode_allowed = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<DiscListBox>'

    @log
    def __init__(self):
        """Initialize"""
        super().__init__(orientation=Gtk.Orientation.VERTICAL)

        self._selection_mode = False
        self._selected_items = []

    @log
    def add(self, widget):
        """Insert a DiscBox widget"""
        super().add(widget)
        widget.connect('selection-changed', self._on_selection_changed)

        self.bind_property(
            'selection-mode', widget, 'selection-mode',
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)
        self.bind_property(
            'selection-mode-allowed', widget, 'selection-mode-allowed',
            GObject.BindingFlags.SYNC_CREATE)

    @log
    def _on_selection_changed(self, widget):
        self.emit('selection-changed')

    @log
    def get_selected_items(self):
        """Returns all selected items for all discs

        :returns: All selected items
        :rtype: A list if Grilo media items
        """
        self._selected_items = []

        def get_child_selected_items(child):
            self._selected_items += child.get_selected_items()

        self.foreach(get_child_selected_items)

        return self._selected_items

    @log
    def select_all(self):
        """Select all songs"""
        def child_select_all(child):
            child.select_all()

        self.foreach(child_select_all)

    @log
    def select_none(self):
        """Deselect all songs"""
        def child_select_none(child):
            child.select_none()

        self.foreach(child_select_none)

    @GObject.Property(type=bool, default=False)
    def selection_mode(self):
        """selection mode getter

        :returns: If selection mode is active
        :rtype: bool
        """
        return self._selection_mode

    @selection_mode.setter
    def selection_mode(self, value):
        """selection-mode setter

        :param bool value: Activate selection mode
        """
        if not self.props.selection_mode_allowed:
            return

        self._selection_mode = value
Ejemplo n.º 5
0
class ApplicationWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'ApplicationWindow'

    client = GObject.Property(type=Client)
    main_box = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    search_revealer = GtkTemplate.Child()
    header_bar = GtkTemplate.Child()
    alt_speed_toggle = GtkTemplate.Child()
    tracker_box = GtkTemplate.Child()
    directory_box = GtkTemplate.Child()
    main_stack = GtkTemplate.Child()
    warning_page = GtkTemplate.Child()
    main_sw = GtkTemplate.Child()
    no_torrents = GtkTemplate.Child()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.init_template()
        self._init_actions()
        self._filter_status = None
        self._filter_error = None
        self._filter_text = None
        self._filter_tracker = None
        self._filter_directory = None
        self._add_dialogs = []
        self._queued_torrents = []

        self._hooks = [
            self.client.connect('notify::download-speed',
                                self._on_speed_refresh),
            self.client.connect('notify::connected',
                                self._on_connected_change),
        ]
        self.client.bind_property('alt-speed-enabled', self.alt_speed_toggle,
                                  'active', GObject.BindingFlags.SYNC_CREATE)

        # Set initial state
        self._on_connected_change()
        self._on_speed_refresh()

        torrent_target = Gtk.TargetEntry.new('text/uri-list',
                                             Gtk.TargetFlags.OTHER_APP, 0)
        self.drag_dest_set(Gtk.DestDefaults.ALL, (torrent_target, ),
                           Gdk.DragAction.MOVE)

        view = TorrentListView(self.client.props.torrents,
                               client=self.client,
                               visible=True)
        self._filter_model = view.filter_model
        self._filter_model.set_visible_func(self._filter_model_func)
        self.main_sw.add(view)

        self._filter_model.connect('row-deleted', self._on_row_deleted)
        self._filter_model.connect('row-inserted', self._on_row_inserted)
        self.no_torrents.props.visible = len(self._filter_model) == 0

    def do_destroy(self):
        for hook in self._hooks:
            self.client.disconnect(hook)
        self._hooks = []
        Gtk.ApplicationWindow.do_destroy(self)

    def _init_actions(self):
        app = self.props.application
        self._add_action = Gio.SimpleAction.new('torrent_add',
                                                GLib.VariantType('s'))
        self._add_action.connect('activate', self._on_torrent_add)
        self.add_action(self._add_action)
        app.set_accels_for_action("win.torrent_add('')", ['<Primary>o'])

        self._add_uri_action = Gio.SimpleAction.new('torrent_add_uri',
                                                    GLib.VariantType('s'))
        self._add_uri_action.connect('activate', self._on_torrent_add)
        self.add_action(self._add_uri_action)
        app.set_accels_for_action("win.torrent_add_uri('')",
                                  ['<Primary>l', '<Primary>u'])

        Action = namedtuple('Action', ('name', 'value', 'callback'))
        actions = (
            Action('filter_status', GLib.Variant('i', -1),
                   self._on_status_filter),
            Action('filter_tracker', GLib.Variant('s', _('Any')),
                   self._on_tracker_filter),
            Action('filter_directory', GLib.Variant('s', _('Any')),
                   self._on_directory_filter),
        )

        for action in actions:
            act = Gio.SimpleAction.new_stateful(action.name,
                                                action.value.get_type(),
                                                action.value)
            act.connect('change-state', action.callback)
            self.add_action(act)

    def _on_connected_change(self, *args):
        if self.client.props.connected:
            self.main_stack.props.visible_child = self.main_box
            while self._queued_torrents:
                self._on_torrent_add_real(*self._queued_torrents.pop(0))
        else:
            self.main_stack.props.visible_child = self.warning_page

    def _on_speed_refresh(self, *args):
        subtitle = ''
        down = self.client.props.download_speed
        up = self.client.props.upload_speed
        if down:
            subtitle += '↓ {}/s'.format(GLib.format_size(down))
        if down and up:
            subtitle += ' — '
        if up:
            subtitle += '↑ {}/s'.format(GLib.format_size(up))
        self.header_bar.props.subtitle = subtitle

    def _on_row_deleted(self, model, path):
        if not self.no_torrents.props.visible and len(model) == 0:
            self.no_torrents.show()

    def _on_row_inserted(self, model, path, iter_):
        if self.no_torrents.props.visible and len(model):
            self.no_torrents.hide()

    @GtkTemplate.Callback
    def _on_alt_speed_toggled(self, button):
        self.client.session_set({'alt-speed-enabled': button.props.active})

    @GtkTemplate.Callback
    def _on_drag_data_received(self, widget, context, x, y, data, info, time):
        success = False

        for uri in data.get_data().split():
            with suppress(UnicodeDecodeError):
                uri = uri.decode('utf-8')
                if uri.endswith('.torrent'):
                    self._add_action.activate(GLib.Variant('s', uri))
                    success = True

        Gtk.drag_finish(context, success, success, time)

    @staticmethod
    @lru_cache(maxsize=1000)
    def _get_torrent_trackers(torrent) -> set:
        trackers = set()
        for tracker in ListStore(torrent.props.trackers):
            tracker_url = urlparse(tracker.props.announce).hostname
            trackers.add(tracker_url)
        return trackers

    @GtkTemplate.Callback
    def _on_filter_button_toggled(self, button):
        if not button.props.active:
            # Empty on close
            self.tracker_box.foreach(lambda child: child.destroy())
            self.directory_box.foreach(lambda child: child.destroy())
            return

        torrents = ListStore(self.client.props.torrents)

        trackers = set()
        for torrent in torrents:
            trackers |= self._get_torrent_trackers(torrent)
        for tracker in [_('Any')] + list(trackers):
            button = Gtk.ModelButton(text=tracker,
                                     action_name='win.filter_tracker',
                                     action_target=GLib.Variant('s', tracker),
                                     visible=True)
            self.tracker_box.add(button)

        # TODO: Might be a better way to show these
        directories = {
            torrent.props.download_dir.rstrip('/')
            for torrent in torrents
        }
        for directory in [_('Any')] + sorted(directories):
            label = directory.rpartition('/')[2]
            if len(label) >= 25:
                label = '…' + label[-24:]
            button = Gtk.ModelButton(text=label,
                                     action_name='win.filter_directory',
                                     action_target=GLib.Variant(
                                         's', directory),
                                     visible=True)
            self.directory_box.add(button)

    @GtkTemplate.Callback
    def _on_search_changed(self, entry):
        text = entry.get_text().lower() or None
        last_value = self._filter_text
        self._filter_text = text
        if last_value != text:
            self._filter_model.refilter()

    def _on_status_filter(self, action, value):
        new_value = value.get_int32()
        action.set_state(value)

        if new_value < 0:
            self._filter_status = None
            self._filter_error = None
        elif new_value >= 10:
            # Hack where we shove errors and status into same value
            self._filter_error = new_value - 10
            self._filter_status = None
        else:
            self._filter_status = new_value
            self._filter_error = None

        self._filter_model.refilter()

    def _on_tracker_filter(self, action, value):
        new_value = value.get_string()

        action.set_state(value)
        if new_value == _('Any'):
            self._filter_tracker = None
        else:
            self._filter_tracker = new_value
        self._filter_model.refilter()

    def _on_directory_filter(self, action, value):
        new_value = value.get_string()

        action.set_state(value)
        if new_value == _('Any'):
            self._filter_directory = None
        else:
            self._filter_directory = new_value
        self._filter_model.refilter()

    @GtkTemplate.Callback
    def _on_search_toggle(self, button):
        active = button.props.active
        self.search_revealer.set_reveal_child(active)
        if not active:
            self.search_entry.props.text = ''
        else:
            self.search_entry.grab_focus()

    def _filter_model_func(self, model, it, data=None) -> bool:
        if self._filter_status is not None and model[it][
                TorrentColumn.status] != self._filter_status:
            return False
        if self._filter_error is not None and model[it][
                TorrentColumn.error] != self._filter_error:
            return False
        if self._filter_text is not None and self._filter_text not in model[
                it][TorrentColumn.name].lower():
            return False
        if self._filter_directory is not None:
            if self._filter_directory != model[it][
                    TorrentColumn.directory].rstrip('/'):
                return False
        if self._filter_tracker is not None:
            return self._filter_tracker in self._get_torrent_trackers(
                model[it][-1])
        return True

    def _on_torrent_add_real(self, uri, uri_only):
        for dialog in self._add_dialogs:
            if dialog.uri == uri:
                dialog.present()
                logging.info('Raising existing dialog for {}'.format(uri))
                return

        if uri_only is True:
            dialog = AddURIDialog(transient_for=self,
                                  uri=uri,
                                  client=self.client)
        else:
            dialog = AddDialog(transient_for=self, uri=uri, client=self.client)
        self._add_dialogs.append(dialog)
        dialog.connect('destroy', lambda d: self._add_dialogs.remove(d))
        dialog.present()

    def _on_torrent_add(self, action, param):
        file = param.get_string()
        uri_only = action is self._add_uri_action
        if self.client.props.connected:
            self._on_torrent_add_real(file, uri_only)
        else:
            self._queued_torrents.append((file, uri_only))
Ejemplo n.º 6
0
class FilterList(Gtk.Box, EditableListWidget):

    __gtype_name__ = "FilterList"

    treeview = Gtk.Template.Child()
    remove = Gtk.Template.Child()
    move_up = Gtk.Template.Child()
    move_down = Gtk.Template.Child()
    pattern_column = Gtk.Template.Child()
    validity_renderer = Gtk.Template.Child()

    default_entry = [_("label"), False, _("pattern"), True]

    filter_type = GObject.Property(
        type=int,
        flags=(GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE
               | GObject.ParamFlags.CONSTRUCT_ONLY),
    )

    settings_key = GObject.Property(
        type=str,
        flags=(GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE
               | GObject.ParamFlags.CONSTRUCT_ONLY),
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.model = self.treeview.get_model()

        self.pattern_column.set_cell_data_func(self.validity_renderer,
                                               self.valid_icon_celldata)

        for filter_params in settings.get_value(self.settings_key):
            filt = FilterEntry.new_from_gsetting(filter_params,
                                                 self.filter_type)
            if filt is None:
                continue
            valid = filt.filter is not None
            self.model.append(
                [filt.label, filt.active, filt.filter_string, valid])

        for signal in ('row-changed', 'row-deleted', 'row-inserted',
                       'rows-reordered'):
            self.model.connect(signal, self._update_filter_string)

        self.setup_sensitivity_handling()

    def valid_icon_celldata(self, col, cell, model, it, user_data=None):
        is_valid = model.get_value(it, 3)
        icon_name = "gtk-dialog-warning" if not is_valid else None
        cell.set_property("stock-id", icon_name)

    @Gtk.Template.Callback()
    def on_add_clicked(self, button):
        self.add_entry()

    @Gtk.Template.Callback()
    def on_remove_clicked(self, button):
        self.remove_selected_entry()

    @Gtk.Template.Callback()
    def on_move_up_clicked(self, button):
        self.move_up_selected_entry()

    @Gtk.Template.Callback()
    def on_move_down_clicked(self, button):
        self.move_down_selected_entry()

    @Gtk.Template.Callback()
    def on_name_edited(self, ren, path, text):
        self.model[path][0] = text

    @Gtk.Template.Callback()
    def on_cellrenderertoggle_toggled(self, ren, path):
        self.model[path][1] = not ren.get_active()

    @Gtk.Template.Callback()
    def on_pattern_edited(self, ren, path, text):
        valid = FilterEntry.check_filter(text, self.filter_type)
        self.model[path][2] = text
        self.model[path][3] = valid

    def _update_filter_string(self, *args):
        value = [(row[0], row[1], row[2]) for row in self.model]
        settings.set_value(self.settings_key, GLib.Variant('a(sbs)', value))
Ejemplo n.º 7
0
class BaseBuddy(GObject.GObject):
    """UI interface for a Buddy in the presence service

    Each buddy interface tracks a set of activities and properties
    that can be queried to provide UI controls for manipulating
    the presence interface.

    Properties Dictionary:
        'key': public key,
        'nick': nickname ,
        'color': color (XXX what format),
        'current-activity': (XXX dbus path?),
        'owner': (XXX dbus path?),
    """

    __gtype_name__ = 'PresenceBaseBuddy'

    __gsignals__ = {
        'joined-activity':
        (GObject.SignalFlags.RUN_FIRST, None, ([GObject.TYPE_PYOBJECT])),
        'left-activity':
        (GObject.SignalFlags.RUN_FIRST, None, ([GObject.TYPE_PYOBJECT])),
        'property-changed':
        (GObject.SignalFlags.RUN_FIRST, None, ([GObject.TYPE_PYOBJECT])),
    }

    def __init__(self):
        GObject.GObject.__init__(self)

        self._key = None
        self._nick = None
        self._color = None
        self._current_activity = None
        self._owner = False
        self._ip4_address = None
        self._tags = None

    def get_key(self):
        return self._key

    def set_key(self, key):
        self._key = key

    key = GObject.Property(type=str, getter=get_key, setter=set_key)

    def get_nick(self):
        return self._nick

    def set_nick(self, nick):
        self._nick = nick

    nick = GObject.Property(type=str, getter=get_nick, setter=set_nick)

    def get_color(self):
        return self._color

    def set_color(self, color):
        self._color = color

    color = GObject.Property(type=str, getter=get_color, setter=set_color)

    def get_current_activity(self):
        if self._current_activity is None:
            return None
        for activity in list(self._activities.values()):
            if activity.props.id == self._current_activity:
                return activity
        return None

    current_activity = GObject.Property(type=object,
                                        getter=get_current_activity)

    def get_owner(self):
        return self._owner

    def set_owner(self, owner):
        self._owner = owner

    owner = GObject.Property(type=bool,
                             getter=get_owner,
                             setter=set_owner,
                             default=False)

    def get_ip4_address(self):
        return self._ip4_address

    def set_ip4_address(self, ip4_address):
        self._ip4_address = ip4_address

    ip4_address = GObject.Property(type=str,
                                   getter=get_ip4_address,
                                   setter=set_ip4_address)

    def get_tags(self):
        return self._tags

    def set_tags(self, tags):
        self._tags = tags

    tags = GObject.Property(type=str, getter=get_tags, setter=set_tags)

    def object_path(self):
        """Retrieve our dbus object path"""
        return None
Ejemplo n.º 8
0
class OpenSubtitles(GObject.Object, Peas.Activatable):
    __gtype_name__ = 'OpenSubtitles'

    object = GObject.Property(type=GObject.Object)
    PROGRESS_INTERVAL = 350
    CACHE_LIFETIME_DAYS = 1
    USER_AGENT = 'Totem'

    def __init__(self):
        GObject.Object.__init__(self)

        self.language = LanguageSetting()
        self.dialog_lock = threading.RLock()

        # Future members
        self.totem_plugin = None
        self.api = None
        self.dialog = None
        self.dialog_action = None
        self.subs_menu = None
        self._set_subtitle_action = None

        # Name of the movie file which the most-recently-downloaded subtitles
        # are related to.
        self.mrl_filename = None

    #####################################################################
    # totem.Plugin methods
    #####################################################################

    def do_activate(self):
        """
        Called when the plugin is activated.
        Here the sidebar page is initialized (set up the treeview, connect
        the callbacks, ...) and added to totem.
        """
        self.totem_plugin = self.object

        # Obtain the ServerProxy and init the model
        self.api = OpenSubtitlesApi(self.USER_AGENT)

        self.totem_plugin.connect('file-opened', self.__on_totem__file_opened)
        self.totem_plugin.connect('file-closed', self.__on_totem__file_closed)

        self.dialog = SearchDialog.create_fake()

        self.dialog_action = Gio.SimpleAction.new("opensubtitles", None)
        self.dialog_action.connect('activate', self.open_dialog)
        self.totem_plugin.add_action(self.dialog_action)
        self.totem_plugin.set_accels_for_action("app.opensubtitles",
                                                ["<Primary><Shift>s"])

        # Append menu item
        menu = self.totem_plugin.get_menu_section(
            "subtitle-download-placeholder")
        menu.append(GT(u'_Search OpenSubtitles...'), "app.opensubtitles")

        self._set_subtitle_action = Gio.SimpleAction.new(
            "set-opensubtitles", GLib.VariantType.new("as"))
        self._set_subtitle_action.connect('activate',
                                          self.__on_menu_set_subtitle)
        self.totem_plugin.add_action(self._set_subtitle_action)

        self.subs_menu = Gio.Menu()
        menu.append_section(None, self.subs_menu)

        # Enable dialog
        enable_dialog = self.totem_plugin.is_playing(
        ) and self.is_support_subtitles()
        self.dialog_action.set_enabled(enable_dialog)

    def do_deactivate(self):
        self.close_dialog()

        # Cleanup menu
        self.totem_plugin.empty_menu_section("subtitle-download-placeholder")

    #####################################################################
    # UI related code
    #####################################################################

    def open_dialog(self, _action, _params):
        if not self.is_support_subtitles():
            return

        with self.dialog_lock:
            self.close_dialog()
            self.dialog = SearchDialog(self, self.totem_plugin,
                                       self.language.list)

        self.dialog.show()
        self.submit_search_request(cached=True, feeling_lucky=False)

    def close_dialog(self):
        with self.dialog_lock:
            self.dialog.close()
            self.dialog = SearchDialog.create_fake()

    def enable(self):
        self.dialog_action.set_enabled(True)
        self.dialog.clear()
        self.dialog.enable_buttons()

    def disable(self):
        self.dialog_action.set_enabled(False)
        self.mrl_filename = None
        self.dialog.clear()
        self.dialog.disable_buttons()

    #####################################################################
    # Subtitles Support
    #####################################################################

    def is_support_subtitles(self, mrl=None):
        if not mrl:
            mrl = self.totem_plugin.get_current_mrl()
        return self.check_supported_scheme(
            mrl) and not self.check_is_audio(mrl)

    @staticmethod
    def check_supported_scheme(mrl):
        current_file = Gio.file_new_for_uri(mrl)
        scheme = current_file.get_uri_scheme()

        unsupported_scheme = ['dvd', 'http', 'dvb', 'vcd']
        return scheme not in unsupported_scheme

    @staticmethod
    def check_is_audio(mrl):
        # FIXME need to use something else here
        # I think we must use video widget metadata but I don't found a way
        # to get this info from python
        return Gio.content_type_guess(mrl, '')[0].split('/')[0] == 'audio'

    ##########################################################
    # Callbacks Handlers
    ##########################################################

    def __on_totem__file_opened(self, _, new_mrl):
        if self.mrl_filename == new_mrl:
            # Check we're not re-opening the same file; if we are, don't
            # clear anything. This happens when we re-load the file with a
            # new set of subtitles, for example
            return

        self.mrl_filename = new_mrl
        # Check if allows subtitles
        if self.is_support_subtitles(new_mrl):
            self.enable()
            feeling_lucky = not self.is_subtitle_exists()
            self.submit_search_request(cached=True,
                                       feeling_lucky=feeling_lucky)
        else:
            self.disable()

    def __on_totem__file_closed(self, _):
        self.disable()

    def __on_menu_set_subtitle(self, _action, params):
        params = {p: params[i] for i, p in enumerate(['name', 'format', 'id'])}
        self.submit_download_request(params)

    #####################################################################
    # Dialog Handlers
    #####################################################################

    def on_close_dialog(self):
        with self.dialog_lock:
            self.dialog = SearchDialog.create_fake()

    def on_language_change(self, index, language):
        plugin_logger.info("Write language %s to index %s", language, index)
        self.language.update_language(index, language)

    def on_search_request(self):
        self.submit_search_request(cached=False, feeling_lucky=False)

    def on_download_request(self, selected_dict):
        self.submit_download_request(selected_dict)

    #####################################################################
    # Subtitles lookup and download
    #####################################################################

    def submit_search_request(self, cached=False, feeling_lucky=False):
        self.submit_background_work(u'Searching subtitles...',
                                    self.search_subtitles, [cached],
                                    self.handle_search_results,
                                    [feeling_lucky])

    def submit_download_request(self, selected_dict):
        self.submit_background_work(u'Downloading subtitles...',
                                    self.download_subtitles, [selected_dict],
                                    self.handle_downloaded_subtitle)

    def search_subtitles(self, cached=False):
        self.clear_cache()

        if cached:
            results = self.read_cached_search_results()
            if results:
                return results

        languages = self.language.term
        movie_file_path = self.movie_file().get_path()
        return self.api.search_subtitles(languages, movie_file_path)

    def download_subtitles(self, selected_dict):
        self.clear_cache()

        subtitle_name = selected_dict['name']
        subtitle_format = selected_dict['format']

        # Lookup the subtitle in the cache
        cached_subtitle = self.cache_file(subtitle_name)
        content = self._read_file(cached_subtitle)
        if not content:
            subtitle_id = selected_dict['id']
            content = self.api.download_subtitles(subtitle_id)
        return self.save_subtitles(content, subtitle_name, subtitle_format)

    def handle_search_results(self, results, feeling_lucky=False):
        if not results:
            return

        self.write_cached_search_results(results)

        lang_order = {l: i for i, l in enumerate(self.language.list)}
        lang_order = defaultdict(lambda: float('inf'), **lang_order)

        results = list(
            sorted([r for r in results if r['SubFormat'] in SUBTITLES_EXT],
                   key=lambda x:
                   (lang_order[x['SubLanguageID']], -float(x['SubRating']))))
        self._populate_submenu(results)
        self._populate_treeview(results)

        if feeling_lucky and len(results) > 0:
            r = results[0]
            selected_dict = {
                'name': r['SubFileName'],
                'format': r['SubFormat'],
                'id': r['IDSubtitleFile']
            }
            self.submit_download_request(selected_dict)

    def _populate_treeview(self, results):
        item_list = []
        for r in results:
            item = [
                LANGUAGES_MAP[r['SubLanguageID']],
                r['SubFileName'],
                r['SubFormat'],
                r['SubRating'],
                r['IDSubtitleFile'],
                r['SubSize'],
            ]
            item_list.append(item)

        self.dialog.populate_treeview(item_list)

    def _populate_submenu(self, results):
        self.subs_menu.remove_all()
        for r in results:
            lang_name = LANGUAGES_MAP[r['SubLanguageID']]
            file_name = r['SubFileName']
            menu_title = u'\t%s: %s' % (lang_name, file_name)
            menu_item = Gio.MenuItem.new(GT(menu_title),
                                         "app.set-opensubtitles")

            data = GLib.Variant(
                'as', [r['SubFileName'], r['SubFormat'], r['IDSubtitleFile']])
            menu_item.set_action_and_target_value("app.set-opensubtitles",
                                                  data)
            self.subs_menu.append_item(menu_item)

    def save_subtitles(self, subtitles, name, extension):
        if not subtitles or not name or not extension:
            return

        # Delete all previous cached subtitle for this file
        for ext in SUBTITLES_EXT:
            # In the cache dir and in the movie dir
            try:
                old_subtitle_file = self.subtitle_file(ext, cache=False)
                old_subtitle_file.delete(None)
            except Exception as e:
                plugin_logger.exception(e)

        save_to_files = [
            self.cache_file(name),
            self.subtitle_file(extension, cache=False),
            self.subtitle_file(extension, cache=True)
        ]

        for i, f in enumerate(save_to_files):
            try:
                self._write_file(f, subtitles)
                # Stop if manage to save in the movie folder
                if i > 0:
                    return f.get_uri()
            except Exception as e:
                print(e)
                continue

        raise Exception("Cannot save subtitle")

    def handle_downloaded_subtitle(self, subtitle_uri):
        if not subtitle_uri:
            return

        self.close_dialog()
        self.totem_plugin.set_current_subtitle(subtitle_uri)

    #####################################################################
    # Filesystem helpers
    #####################################################################

    def cached_search_results_file(self):
        return self.cache_file("%s.%s" % (self.movie_name(), "opensubtitles"))

    def read_cached_search_results(self):
        result_cache = self.cached_search_results_file()
        data = self._read_file(result_cache)
        if not data:
            return
        try:
            if sys.version_info[0] < 3:
                data = str(data)
            else:
                data = str(data, 'utf-8')
            return literal_eval(data)
        except:
            return

    def write_cached_search_results(self, results):
        result_cache = self.cached_search_results_file()
        file_content = pprint.pformat(results).encode('utf-8')
        self._write_file(result_cache, file_content)

    @staticmethod
    def _write_file(file_obj, content):
        flags = Gio.FileCreateFlags.REPLACE_DESTINATION
        stream = file_obj.replace('', False, flags, None)
        try:
            stream.write(content, None)
        finally:
            stream.close()

    @staticmethod
    def _read_file(file_obj):
        if not file_obj.query_exists():
            return None
        _, content, _ = file_obj.load_contents()
        if not content:
            return None
        return content

    def movie_name(self):
        subtitle_file = Gio.file_new_for_uri(self.mrl_filename)
        return subtitle_file.get_basename().rpartition('.')[0]

    def movie_file(self):
        return Gio.file_new_for_uri(self.mrl_filename)

    def subtitle_path(self, ext, cache=False):
        movie_name = self.movie_name()
        if cache:
            dir_path = self._cache_subtitles_dir()
        else:
            dir_path = self._movie_dir()
        return os.path.join(dir_path, "%s.%s" % (movie_name, ext))

    def subtitle_file(self, ext, cache=False):
        return Gio.file_new_for_path(self.subtitle_path(ext, cache))

    def is_subtitle_exists(self):
        return any(
            self.subtitle_file(ext, cache=False).query_exists()
            for ext in SUBTITLES_EXT)

    def cache_file(self, filename):
        dir_path = self._cache_subtitles_dir()
        directory = Gio.file_new_for_path(dir_path)
        if not directory.query_exists():
            directory.make_directory_with_parents(None)
        file_path = os.path.join(dir_path, filename)
        return Gio.file_new_for_path(file_path)

    @staticmethod
    def _cache_subtitles_dir():
        bpath = GLib.get_user_cache_dir()
        ret = os.path.join(bpath, 'totem', 'subtitles')
        GLib.mkdir_with_parents(ret, 0o777)
        return ret

    def _movie_dir(self):
        directory = Gio.file_new_for_uri(self.mrl_filename)
        parent = directory.get_parent()
        return parent.get_path()

    def clear_cache(self):
        dir_path = self._cache_subtitles_dir()
        directory = Gio.file_new_for_path(dir_path)
        children = directory.enumerate_children(
            "time::modified,standard::name", Gio.FileQueryInfoFlags.NONE, None)

        current_time = datetime.datetime.fromtimestamp(time.time())
        for d in children:
            modified = datetime.datetime.fromtimestamp(
                d.get_attribute_uint64("time::modified"))
            days = (current_time - modified).total_seconds() / SECONDS_PER_DAY
            if days > self.CACHE_LIFETIME_DAYS:
                plugin_logger.info("Delete: %s", d.get_name())
                path = os.path.join(dir_path, d.get_name())
                Gio.file_new_for_path(path).delete(None)

    ########################################################
    # Background Work
    ########################################################

    def submit_background_work(self,
                               init_message,
                               work_func,
                               work_args,
                               callback_func,
                               callback_args=()):
        work_tracker = {
            "status": False,
            "callback": (callback_func, callback_args)
        }
        self.init_progress(work_tracker, init_message)
        args = [work_tracker, work_func]
        args.extend(work_args)
        threading.Thread(target=self.__background_work, args=args).start()

    @staticmethod
    def __background_work(work_tracker, work_func, *args):
        result = None
        message = None

        try:
            result = work_func(*args)
            message = "Success (%s)" % len(result)
        except Exception as e:
            plugin_logger.exception(e)
            result = None
            message = str(e)
        finally:
            work_tracker["result"] = result
            work_tracker["message"] = message
            work_tracker["status"] = True

    def init_progress(self, work_tracker, message):
        try:
            self.dialog.set_progress_message(message)
            self.dialog.disable_buttons()
            self.dialog.start_loading_animation()
            self.progress(work_tracker)
            GLib.timeout_add(self.PROGRESS_INTERVAL, self.progress,
                             work_tracker)
        except Exception as e:
            plugin_logger.exception(e)
            self.dialog.set_progress_message(str(e))

    def progress(self, work_tracker):
        self.dialog.progress_pulse()
        if not work_tracker["status"]:
            return True

        callback, args = work_tracker["callback"]
        result = work_tracker["result"]
        message = work_tracker["message"]

        try:
            callback(result, *args)
        except Exception as e:
            plugin_logger.exception(e)
            if not message:
                message = str(e)
            else:
                message = "%s, but %s" % (message, str(e))

        self.dialog.enable_buttons()
        self.dialog.stop_loading_animation()
        self.dialog.set_progress_message(message)
        self.dialog.progress_reset()
        return False
Ejemplo n.º 9
0
class Debugger(GObject.Object):
    debug_info: lib.DebugInfo
    tempdir: tempfile.TemporaryDirectory  # Used for unsaved files
    path: str  # Path of the compiled file

    subprocpid: int  # PID of the process
    subprocpid_lock: threading.RLock

    @GObject.Property
    def running(self) -> bool:
        self.subprocpid_lock.acquire()
        running = self.subprocpid is not None
        self.subprocpid_lock.release()
        return running
    use_idle = GObject.Property(default=False, type=bool)

    @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST,
                    arg_types=(DebuggingReuslt,))
    def debugging_done(self, *args):
        pass

    def __init__(self,
                 project: lib.ProjectBuffer,
                 compiler: lib.Compiler, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.tempdir = None
        self.subprocpid = None
        self.subprocpid_lock = threading.RLock()

        code, debug_info = compiler.compile(project.code.lines)
        self.debug_info = debug_info
        utils.debug('Generated code:')
        utils.debug(code)

        self.path = None
        if project._project_file:
            self.path = project._project_file.get_path() + '.py'
        else:
            self.tempdir = tempfile.TemporaryDirectory(prefix='turtlico_')
            self.path = os.path.join(self.tempdir.name, 'program.py')

        assert isinstance(self.path, str)
        with open(self.path, 'w') as f:
            f.write(code)

        self.props.use_idle = project.props.run_in_console

    def dispose(self):
        if self.tempdir:
            self.tempdir.cleanup()
        if self.props.running:
            self.stop()

    def run(self):
        if self.props.running:
            return
        # Sets something to subprocpid in order to prevent
        # from starting two threads at once
        self.subprocpid_lock.acquire()
        self.subprocpid = -1
        self.subprocpid_lock.release()

        self.props.running = True
        # The child program is run as a separate process due to safety reasons
        launcher = _launcher.format(self.path)
        if self.props.use_idle:
            launcher += _idle_exit.format(
                _('Press enter to close this window'))

        args = [_get_python()]
        if self.props.use_idle:
            args.extend(['-m', 'idlelib', '-t', 'Turtlico'])
        args.extend(['-c', launcher])

        thread = threading.Thread(
            target=self._run_child, args=[args], daemon=True)
        thread.start()

    def stop(self):
        self.subprocpid_lock.acquire()
        assert self.props.running is True

        platform = sys.platform
        try:
            if platform == 'win32':
                os.kill(self.subprocpid, signal.CTRL_C_EVENT)
            else:
                os.kill(self.subprocpid, signal.SIGKILL)
        except Exception as e:
            utils.error(f'Cannot stop debugging: "{e}"')
        self.subprocpid_lock.release()

    def _run_child(self, args):
        self.subprocpid_lock.acquire()

        subproc = subprocess.Popen(
            args,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self.subprocpid = subproc.pid

        self.subprocpid_lock.release()

        stdout, stderr = subproc.communicate()
        GLib.idle_add(self._run_child_done, stderr.decode('utf-8'))

    def _run_child_done(self, stderr: str) -> bool:
        self.subprocpid_lock.acquire()
        self.subprocpid = None
        self.subprocpid_lock.release()

        result = DebuggingReuslt(self.debug_info, stderr)

        self.emit('debugging-done', result)
        return GLib.SOURCE_REMOVE
Ejemplo n.º 10
0
class HTTPRequest(GObject.Object):
    """
    Class encapsulating a single HTTP request. These are meant to be sent
    and received only once. Behaviour is undefined otherwise.
    """

    __gsignals__ = {
        # Successes
        'sent': (SignalFlags.RUN_LAST, None, (Soup.Message, )),
        'received': (SignalFlags.RUN_LAST, None, (Gio.OutputStream, )),
        # Failures
        'send-failure': (SignalFlags.RUN_LAST, None, (object, )),
        'receive-failure': (SignalFlags.RUN_LAST, None, (object, )),
        # Common failure signal which will be emitted when either of above
        # failure signals are.
        'failure': (SignalFlags.RUN_LAST, None, (object, )),
    }

    message = GObject.Property(type=Soup.Message,
                               flags=PARAM_READWRITECONSTRUCT)
    cancellable = GObject.Property(type=Gio.Cancellable,
                                   flags=PARAM_READWRITECONSTRUCT)
    istream = GObject.Property(type=Gio.InputStream, default=None)
    ostream = GObject.Property(type=Gio.OutputStream, default=None)

    def __init__(self, message, cancellable):
        if message is None:
            raise ValueError('Message may not be None')

        inner_cancellable = Gio.Cancellable()
        super(HTTPRequest, self).__init__(message=message,
                                          cancellable=inner_cancellable)
        if cancellable is not None:
            cancellable.connect(lambda *x: self.cancel(), None)

        self.connect('send-failure', lambda r, e: r.emit('failure', e))
        self.connect('receive-failure', lambda r, e: r.emit('failure', e))

        # For simple access
        self._receive_started = False
        self._uri = self.message.get_uri().to_string(False)

    def send(self):
        """
        Send the request and receive HTTP headers. Some of the body might
        get downloaded too.
        """
        session.send_async(self.message, self.cancellable, self._sent, None)

    def _sent(self, session, task, data):
        try:
            status = int(self.message.get_property('status-code'))
            if status >= 400:
                msg = 'HTTP {0} error in {1} request to {2}'.format(
                    status, self.message.method, self._uri)
                print_w(msg)
                return self.emit('send-failure', Exception(msg))
            self.istream = session.send_finish(task)
            print_d('Got HTTP {code} on {method} request to {uri}.'.format(
                uri=self._uri, code=status, method=self.message.method))
            self.emit('sent', self.message)
        except GLib.GError as e:
            print_w('Failed sending {method} request to {uri} ({err})'.format(
                method=self.message.method, uri=self._uri, err=e))
            self.emit('send-failure', e)

    def provide_target(self, stream):
        if not stream:
            raise ValueError('Provided stream may not be None')
        if not self.ostream:
            self.ostream = stream
        else:
            raise RuntimeError('Only one output stream may be provided')

    def cancel(self):
        """
        Cancels the future and currently running HTTPRequest actions.

        It is safe to run this function before, during and after any action
        made with HTTPRequest.

        After HTTPRequest is cancelled, one usually would not do any more
        actions with it. However, it is safe to do something after
        cancellation, but those actions usually will fail.
        """

        if self.cancellable.is_cancelled():
            return False
        self.cancellable.cancel()

        # If we already have input stream, we can just close it, message
        # will come out as cancelled just fine.
        if self.istream and not self._receive_started:
            if not self.istream.is_closed():
                self.istream.close(None)
        else:
            session.cancel_message(self.message, Soup.Status.CANCELLED)

    def receive(self):
        """
        Receive data from the request into provided output stream. The request
        must be already sent, therefore this function will be usually called
        from the 'sent' signal handler.

        On completion of data receipt, HTTPRequest lifetime is ended and
        inner resources are cleaned up (except persistent connections that are
        part of session, not request).

        .. note::
        Be sure to clean up resources you've allocated yourself (e.g. close
        GOutputStreams, delete files on failure et cetera).
        """
        if not self.istream:
            raise RuntimeError('Cannot receive unsent request')
        if not self.ostream:
            raise RuntimeError('Cannot receive request without output stream')
        if self._receive_started:
            raise RuntimeError('Can receive only once')
        self._receive_started = True

        def spliced(ostream, task, data):
            try:
                ostream.splice_finish(task)
                self.istream.close(None)
                self.emit('received', ostream)
            except GLib.GError as e:
                self.istream.close(None)
                self.emit('receive-failure', e)

        # Do not ask splice to close the stream as Soup gets confused and
        # doesn't close connections
        # https://bugzilla.gnome.org/show_bug.cgi?id=711260
        flags = Gio.OutputStreamSpliceFlags.NONE
        self.ostream.splice_async(self.istream, flags, GLib.PRIORITY_DEFAULT,
                                  self.cancellable, spliced, None)
Ejemplo n.º 11
0
class ToolButton(Gtk.ToolButton):
    '''
    The ToolButton class manages a Gtk.ToolButton styled for Sugar.

    Keyword Args:
        icon_name(string): name of themed icon.

        accelerator (string): keyboard shortcut to be used to
            activate this button.

        tooltip (string): tooltip to be displayed when user hovers
            over button.

        hide_tooltip_on_click (bool): Whether or not the tooltip
            is hidden when user clicks on button.

    '''

    __gtype_name__ = 'SugarToolButton'

    def __init__(self, icon_name=None, **kwargs):
        self._accelerator = None
        self._tooltip = None
        self._palette_invoker = ToolInvoker()

        GObject.GObject.__init__(self, **kwargs)

        self._hide_tooltip_on_click = True
        self._palette_invoker.attach_tool(self)

        if icon_name:
            self.set_icon_name(icon_name)

        self.get_child().connect('can-activate-accel',
                                 self.__button_can_activate_accel_cb)

        self.connect('destroy', self.__destroy_cb)

    def __destroy_cb(self, icon):
        if self._palette_invoker is not None:
            self._palette_invoker.detach()

    def __button_can_activate_accel_cb(self, button, signal_id):
        # Accept activation via accelerators regardless of this widget's state
        return True

    def set_tooltip(self, tooltip):
        '''
        Set the tooltip.

        Args:
            tooltip (string): tooltip to be set.
        '''
        if self.palette is None or self._tooltip is None:
            self.palette = Palette(tooltip)
        elif self.palette is not None:
            self.palette.set_primary_text(tooltip)

        self._tooltip = tooltip

        # Set label, shows up when toolbar overflows
        Gtk.ToolButton.set_label(self, tooltip)

    def get_tooltip(self):
        '''
        Return the tooltip.
        '''
        return self._tooltip

    tooltip = GObject.Property(type=str,
                               setter=set_tooltip,
                               getter=get_tooltip)

    def get_hide_tooltip_on_click(self):
        '''
        Return True if the tooltip is hidden when a user
        clicks on the button, otherwise return False.
        '''
        return self._hide_tooltip_on_click

    def set_hide_tooltip_on_click(self, hide_tooltip_on_click):
        '''
        Set whether or not the tooltip is hidden when a user
        clicks on the button.

        Args:
            hide_tooltip_on_click (bool): True if the tooltip is
            hidden on click, and False otherwise.
        '''
        if self._hide_tooltip_on_click != hide_tooltip_on_click:
            self._hide_tooltip_on_click = hide_tooltip_on_click

    hide_tooltip_on_click = GObject.Property(type=bool,
                                             default=True,
                                             getter=get_hide_tooltip_on_click,
                                             setter=set_hide_tooltip_on_click)

    def set_accelerator(self, accelerator):
        '''
        Set accelerator that activates the button.

        Args:
            accelerator(string): accelerator to be set.
        '''
        self._accelerator = accelerator
        setup_accelerator(self)

    def get_accelerator(self):
        '''
        Return accelerator that activates the button.
        '''
        return self._accelerator

    accelerator = GObject.Property(type=str,
                                   setter=set_accelerator,
                                   getter=get_accelerator)

    def set_icon_name(self, icon_name):
        '''
        Set name of icon.

        Args:
            icon_name (string): name of icon
        '''
        icon = Icon(icon_name=icon_name)
        self.set_icon_widget(icon)
        icon.show()

    def get_icon_name(self):
        '''
        Return icon name, or None if there is no icon name.
        '''
        if self.props.icon_widget is not None:
            return self.props.icon_widget.props.icon_name
        else:
            return None

    icon_name = GObject.Property(type=str,
                                 setter=set_icon_name,
                                 getter=get_icon_name)

    def create_palette(self):
        return None

    def get_palette(self):
        return self._palette_invoker.palette

    def set_palette(self, palette):
        self._palette_invoker.palette = palette

    palette = GObject.Property(type=object,
                               setter=set_palette,
                               getter=get_palette)

    def get_palette_invoker(self):
        return self._palette_invoker

    def set_palette_invoker(self, palette_invoker):
        self._palette_invoker.detach()
        self._palette_invoker = palette_invoker

    palette_invoker = GObject.Property(type=object,
                                       setter=set_palette_invoker,
                                       getter=get_palette_invoker)

    def do_draw(self, cr):
        '''
        Implementation method for drawing the button.
        '''
        if self.palette and self.palette.is_up():
            allocation = self.get_allocation()
            # draw a black background, has been done by the engine before
            cr.set_source_rgb(0, 0, 0)
            cr.rectangle(0, 0, allocation.width, allocation.height)
            cr.paint()

        Gtk.ToolButton.do_draw(self, cr)

        if self.palette and self.palette.is_up():
            invoker = self.palette.props.invoker
            invoker.draw_rectangle(cr, self.palette)

        return False

    def do_clicked(self):
        '''
        Implementation method for hiding the tooltip when
        the button is clicked.
        '''
        if self._hide_tooltip_on_click and self.palette:
            self.palette.popdown(True)
Ejemplo n.º 12
0
class ColorPickerViewActivatable(GObject.Object, Gedit.ViewActivatable):

    view = GObject.Property(type=Gedit.View)

    def __init__(self):
        GObject.Object.__init__(self)
        self._rgba_str = None
        self._color_button = None
        self._color_helper = ColorHelper()

    def do_activate(self):

        buf = self.view.get_buffer()
        buf.connect_after('mark-set', self.on_buffer_mark_set)

    def do_deactivate(self):
        if self._color_button is not None:
            self._color_button.destroy()
            self._color_button = None

    def on_buffer_mark_set(self, buf, location, mark):

        if not buf.get_has_selection():
            if self._color_button:
                self._color_button.destroy()
                self._color_button = None
            return

        if mark != buf.get_insert() and mark != buf.get_selection_bound():
            return

        rgba_str = self._color_helper.get_current_color(self.view.get_buffer(), True)
        if rgba_str is not None and rgba_str != self._rgba_str and self._color_button is not None:
            rgba = Gdk.RGBA()
            parsed = rgba.parse(rgba_str)
            if parsed:
                self._rgba_str = rgba_str
                self._color_button.set_rgba(rgba)
        elif rgba_str is not None and self._color_button is None:
            rgba = Gdk.RGBA()
            parsed = rgba.parse(rgba_str)
            if parsed:
                self._rgba_str = rgba_str

                bounds = buf.get_selection_bounds()
                if bounds != ():
                    self._color_button = Gtk.ColorButton.new_with_rgba(rgba)
                    self._color_button.set_halign(Gtk.Align.START)
                    self._color_button.set_valign(Gtk.Align.START)
                    self._color_button.show()
                    self._color_button.connect('color-set', self.on_color_set)

                    start, end = bounds
                    location = self.view.get_iter_location(start)
                    min_width, nat_width = self._color_button.get_preferred_width()
                    min_height, nat_height = self._color_button.get_preferred_height()
                    x = location.x
                    if location.y - nat_height > 0:
                        y = location.y - nat_height
                    else:
                        y = location.y + location.height

                    self.view.add_child_in_window(self._color_button, Gtk.TextWindowType.TEXT, x, y)
        elif not rgba_str and self._color_button is not None:
            self._color_button.destroy()
            self._color_button = None

    def on_color_set(self, color_button):
        rgba = color_button.get_rgba()

        self._color_helper.insert_color(self.view,
                                        "%02x%02x%02x" % (self._color_helper.scale_color_component(rgba.red),
                                                          self._color_helper.scale_color_component(rgba.green),
                                                          self._color_helper.scale_color_component(rgba.blue)))
Ejemplo n.º 13
0
class Keyring(GObject.GObject):
    ID: str = "com.github.bilelmoussaoui.Authenticator"
    PasswordID: str = "com.github.bilelmoussaoui.Authenticator.Login"
    PasswordState: str = "com.github.bilelmoussaoui.Authenticator.State"
    instance: 'Keyring' = None

    can_be_locked: GObject.Property = GObject.Property(type=bool, default=False)

    def __init__(self):
        GObject.GObject.__init__(self)
        service = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS, None)
        service.unlock_sync(service.get_collections(), None)

        self.schema = Secret.Schema.new(Keyring.ID,
                                        Secret.SchemaFlags.NONE,
                                        {
                                            "id": Secret.SchemaAttributeType.STRING,
                                            "name": Secret.SchemaAttributeType.STRING,
                                        })
        self.password_schema = Secret.Schema.new(Keyring.PasswordID,
                                                 Secret.SchemaFlags.NONE,
                                                 {"password": Secret.SchemaAttributeType.STRING})
        self.password_state_schema = Secret.Schema.new(Keyring.PasswordState,
                                                       Secret.SchemaFlags.NONE,
                                                       {"state": Secret.SchemaAttributeType.STRING})
        self.props.can_be_locked = self.is_password_enabled() and self.has_password()

    @staticmethod
    def get_default():
        if Keyring.instance is None:
            Keyring.instance = Keyring()
        return Keyring.instance

    def get_by_id(self, token_id: str) -> str:
        """
        Return the OTP token based on a secret ID.

        :param token_id: the secret ID associated to an OTP token
        :type token_id: str
        :return: the secret OTP token.
        """
        token = Secret.password_lookup_sync(self.schema, {"id": str(token_id)},
                                            None)
        return token

    def insert(self, token_id: str, provider: str, username: str, token: str):
        """
        Save a secret OTP token.

        :param token_id: The secret ID associated to the OTP token
        :param provider: the provider name
        :param username: the username
        :param token: the secret OTP token.


        """
        data = {
            "id": str(token_id),
            "name": str(username),
        }
        Secret.password_store_sync(
            self.schema,
            data,
            Secret.COLLECTION_DEFAULT,
            "{provider} OTP ({username})".format(provider=provider,
                                                 username=username),
            token,
            None
        )

    def remove(self, token_id: str) -> bool:
        """
        Remove a specific secret OTP token.

        :param secret_id: the secret ID associated to the OTP token
        :return bool: Either the token was removed successfully or not
        """
        success = Secret.password_clear_sync(self.sechema, {"id": str(token_id)},
                                             None)
        return success

    def clear(self) -> bool:
        """
           Clear all existing accounts.

           :return bool: Either the token was removed successfully or not
       """
        success = Secret.password_clear_sync(self.schema, {}, None)
        return success

    def get_password(self) -> str:
        password = Secret.password_lookup_sync(self.password_schema, {}, None)
        return password

    def set_password(self, password: str):
        # Clear old password
        self.remove_password()
        # Store the new one
        Secret.password_store_sync(
            self.password_schema,
            {},
            Secret.COLLECTION_DEFAULT,
            "Authenticator password",
            password,
            None
        )
        self.set_password_state(True)

    def is_password_enabled(self) -> bool:
        state = Secret.password_lookup_sync(self.password_state_schema, {}, None)
        return state == 'true' if state else False

    def set_password_state(self, state: bool):
        if not state:
            Secret.password_clear_sync(self.password_state_schema, {}, None)
        else:
            Secret.password_store_sync(
                self.password_state_schema,
                {},
                Secret.COLLECTION_DEFAULT,
                "Authenticator state",
                "true",
                None
            )
        self.props.can_be_locked = state and self.has_password()

    def has_password(self) -> bool:
        return self.get_password() is not None

    def remove_password(self):
        Secret.password_clear_sync(self.password_schema, {}, None)
        self.set_password_state(False)
Ejemplo n.º 14
0
class ViewerPage(Gtk.Widget):
    __gtype_name__ = "ViewerPage"

    pdfviewer     = Gtk.Template.Child()
    main_stack    = Gtk.Template.Child()
    errorlist     = Gtk.Template.Child()
    warninglist   = Gtk.Template.Child()
    badboxlist    = Gtk.Template.Child()
    warning_label = Gtk.Template.Child()
    badbox_label  = Gtk.Template.Child()

    has_error = GObject.Property(type=bool, default=False)
    busy = GObject.Property(type=bool, default=False)

    def __init__(self):
        super().__init__()

        layout = Gtk.BinLayout()
        self.set_layout_manager(layout)
        self.logprocessor = LogProcessor()

    def set_file(self, file):
        self.logprocessor.set_log_path(file.get_log_path())
        self.pdfviewer.set_path(file.get_pdf_path())

    def load_pdf(self):
        self.pdfviewer.load()
        self.main_stack.set_visible_child_name("pdfview")

    def load_log(self):
        for lst in [self.errorlist, self.warninglist, self.badboxlist]:
            c = lst.get_first_child()
            while c:
                lst.remove(c)
                c = lst.get_first_child()
        self.logprocessor.process(self.load_log_finish)

    def load_log_finish(self):
        for e in self.logprocessor.error_list:
            row = Adw.ActionRow.new()
            row.set_activatable(True)
            row.data = e
            row.set_title(f"{e[0]}: \"{e[2]}\" on line {e[1]}")
            self.errorlist.append(row)
            row.connect("activated", self.error_row_activated)
        for e in self.logprocessor.warning_list:
            row = Adw.ActionRow.new()
            row.set_activatable(True)
            row.data = e
            row.set_title(f"{e[0]}: \"{e[2]}\" on line {e[1]}")
            self.warninglist.append(row)
            row.connect("activated", self.error_row_activated)
        for e in self.logprocessor.badbox_list:
            row = Adw.ActionRow.new()
            row.set_activatable(True)
            row.data = e
            row.set_title(f"{e[0]}: \"{e[2]}\" on line {e[1]}")
            self.badboxlist.append(row)
            row.connect("activated", self.error_row_activated)

        if self.logprocessor.error_list:
            self.main_stack.set_visible_child_name("errorview")
            self.set_property("has_error", True)
        else:
            self.set_property("has_error", False)
        if self.logprocessor.warning_list:
            self.warning_label.set_visible(True)
            self.warninglist.set_visible(True)
        else:
            self.warning_label.set_visible(False)
            self.warninglist.set_visible(False)
        if self.logprocessor.badbox_list:
            self.badbox_label.set_visible(True)
            self.badboxlist.set_visible(True)
        else:
            self.badbox_label.set_visible(False)
            self.badboxlist.set_visible(False)
        
    def error_row_activated(self, row):
        path = self.pdfviewer.path[:-3]+ "tex"
        context = row.data[2]
        if context.startswith("..."):
            context = context[3:]
        self.get_root().goto_tex(path, row.data[1], context, -1)
Ejemplo n.º 15
0
class GitWindowActivatable(GObject.Object, Bedit.WindowActivatable):
    window = GObject.Property(type=Bedit.Window)

    windows = weakref.WeakValueDictionary()

    def __init__(self):
        super().__init__()

        self.view_activatables = weakref.WeakSet()

    @classmethod
    def register_view_activatable(cls, view_activatable):
        window = view_activatable.view.get_toplevel()

        if window not in cls.windows:
            return None

        window_activatable = cls.windows[window]

        window_activatable.view_activatables.add(view_activatable)
        view_activatable.connect("notify::status",
                                 window_activatable.notify_status)

        return window_activatable

    def do_activate(self):
        # self.window is not set until now
        self.windows[self.window] = self

        self.app_activatable = GitAppActivatable.get_instance()

        self.bus = self.window.get_message_bus()

        self.git_status_thread = GitStatusThread(self.update_location)
        self.git_status_thread.start()

        self.file_nodes = FileNodes()
        self.monitors = {}
        self.has_focus = True

        self.gobject_signals = {
            self.window: [
                self.window.connect("tab-removed", self.tab_removed),
                self.window.connect("focus-in-event", self.focus_in_event),
                self.window.connect("focus-out-event", self.focus_out_event),
            ],
            # BeditMessageBus.connect() shadows GObject.connect()
            self.bus: [
                GObject.Object.connect(self.bus, "unregistered",
                                       self.unregistered)
            ],
        }

        # It is safe to connect to these even
        # if the file browser is not enabled yet
        self.bus_signals = [
            self.bus.connect(
                "/plugins/filebrowser",
                "root_changed",
                self.root_changed,
                None,
            ),
            self.bus.connect("/plugins/filebrowser", "inserted", self.inserted,
                             None),
            self.bus.connect("/plugins/filebrowser", "deleted", self.deleted,
                             None),
        ]

        self.refresh()

    def do_deactivate(self):
        self.clear_monitors()
        self.git_status_thread.terminate()

        for gobject, sids in self.gobject_signals.items():
            for sid in sids:
                # BeditMessageBus.disconnect() shadows GObject.disconnect()
                GObject.Object.disconnect(gobject, sid)

        for sid in self.bus_signals:
            self.bus.disconnect(sid)

        self.file_nodes = FileNodes()
        self.gobject_signals = {}
        self.bus_signals = []

        self.refresh()

    def refresh(self):
        if self.bus.is_registered("/plugins/filebrowser", "refresh"):
            self.bus.send("/plugins/filebrowser", "refresh")

    def get_view_activatable_by_view(self, view):
        for view_activatable in self.view_activatables:
            if view_activatable.view == view:
                return view_activatable

        return None

    def get_view_activatable_by_location(self, location):
        for view_activatable in self.view_activatables:
            buf = view_activatable.view.get_buffer()
            if buf is None:
                continue

            view_location = buf.get_file().get_location()
            if view_location is None:
                continue

            if view_location.equal(location):
                return view_activatable

        return None

    def notify_status(self, view_activatable, psepc):
        location = (
            view_activatable.view.get_buffer().get_file().get_location())
        if location is None:
            return

        if location not in self.file_nodes:
            return

        repo = self.get_repository(location)
        if repo is not None:
            self.git_status_thread.push(repo, location)

    def tab_removed(self, window, tab):
        view = tab.get_view()

        # Need to remove the view activatable otherwise update_location()
        # might use the view's status and not the file's actual status
        view_activatable = self.get_view_activatable_by_view(view)
        if view_activatable is not None:
            self.view_activatables.remove(view_activatable)

        location = view.get_buffer().get_file().get_location()
        if location is None:
            return

        if location not in self.file_nodes:
            return

        repo = self.get_repository(location)
        if repo is not None:
            self.git_status_thread.push(repo, location)

    def focus_in_event(self, window, event):
        # Enables the file monitors so they can cause things
        # to update again. We disabled them when the focus
        # was lost and we will instead do a full update now.
        self.has_focus = True

        self.app_activatable.clear_repositories()

        for view_activatable in self.view_activatables:
            # Must reload the location's contents, not just rediff
            GLib.idle_add(view_activatable.update_location)

        for location in self.file_nodes:
            # Still need to update the git status
            # as the file could now be in .gitignore
            repo = self.get_repository(location)
            if repo is not None:
                self.git_status_thread.push(repo, location)

    def focus_out_event(self, window, event):
        # Disables the file monitors so they don't
        # cause anything to update. We will do a
        # full update when we have focus again.
        self.has_focus = False

    def unregistered(self, bus, object_path, method):
        # Avoid warnings like crazy if the file browser becomes disabled
        if object_path == "/plugins/filebrowser" and method == "root_changed":
            self.clear_monitors()
            self.git_status_thread.clear()
            self.file_nodes = FileNodes()

    def get_repository(self, location, is_dir=False):
        return self.app_activatable.get_repository(location, is_dir)

    def root_changed(self, bus, msg, data=None):
        self.clear_monitors()
        self.git_status_thread.clear()
        self.file_nodes = FileNodes()

        location = msg.location

        repo = self.get_repository(location, True)
        if repo is not None:
            self.monitor_directory(location)

    def inserted(self, bus, msg, data=None):
        location = msg.location

        repo = self.get_repository(location, msg.is_directory)
        if repo is None:
            return

        if msg.is_directory:
            self.monitor_directory(location)

        else:
            self.file_nodes[location] = FileNode(msg)
            self.git_status_thread.push(repo, location)

    def deleted(self, bus, msg, data=None):
        location = msg.location
        uri = location.get_uri()

        if uri in self.monitors:
            self.monitors[uri].cancel()
            del self.monitors[uri]

        else:
            try:
                del self.file_nodes[location]

            except KeyError:
                pass

    def update_location(self, result):
        location, status = result

        # The node may have been deleted
        # before the status was determined
        try:
            file_node = self.file_nodes[location]

        except KeyError:
            return

        if status is None or not status & Ggit.StatusFlags.IGNORED:
            view_activatable = self.get_view_activatable_by_location(location)
            if view_activatable is not None:
                status = view_activatable.status

        markup = GLib.markup_escape_text(file_node.name)

        if status is not None:
            if (status & Ggit.StatusFlags.INDEX_NEW
                    or status & Ggit.StatusFlags.WORKING_TREE_NEW
                    or status & Ggit.StatusFlags.INDEX_MODIFIED
                    or status & Ggit.StatusFlags.WORKING_TREE_MODIFIED):
                markup = '<span weight="bold">%s</span>' % (markup)

            elif (status & Ggit.StatusFlags.INDEX_DELETED
                  or status & Ggit.StatusFlags.WORKING_TREE_DELETED):
                markup = '<span strikethrough="true">%s</span>' % (markup)

        self.bus.send_sync(
            "/plugins/filebrowser",
            "set_markup",
            id=file_node.id,
            markup=markup,
        )

    def clear_monitors(self):
        for uri in self.monitors:
            self.monitors[uri].cancel()

        self.monitors = {}

    def monitor_directory(self, location):
        try:
            monitor = location.monitor(Gio.FileMonitorFlags.NONE, None)

        except GLib.Error as e:
            debug('Failed to monitor directory "%s": %s' %
                  (location.get_uri(), e))
            return

        self.monitors[location.get_uri()] = monitor
        monitor.connect("changed", self.monitor_changed)

    def monitor_changed(self, monitor, file_a, file_b, event_type):
        # Don't update anything as we will do
        # a full update when we have focus again
        if not self.has_focus:
            return

        # Only monitor for changes as the file browser
        # will emit signals for the other event types
        if event_type != Gio.FileMonitorEvent.CHANGED:
            return

        for f in (file_a, file_b):
            if f is None:
                continue

            # Must let the view activatable know
            # that its location's contents have changed
            view_activatable = self.get_view_activatable_by_location(f)
            if view_activatable is not None:
                # Must reload the location's contents, not just rediff
                GLib.idle_add(view_activatable.update_location)

                # Still need to update the git status
                # as the file could now be in .gitignore

            if f in self.file_nodes:
                repo = self.get_repository(f)
                if repo is not None:
                    self.git_status_thread.push(repo, f)
Ejemplo n.º 16
0
class HistoryCombo(Gtk.ComboBox):
    __gtype_name__ = "HistoryCombo"

    history_id = GObject.Property(
        type=str,
        nick="History ID",
        blurb="Identifier associated with entry's history store",
        default=None,
        flags=GObject.ParamFlags.READWRITE,
    )

    history_length = GObject.Property(
        type=int,
        nick="History length",
        blurb="Number of history items to display in the combo",
        minimum=1,
        maximum=20,
        default=HISTORY_ENTRY_HISTORY_LENGTH_DEFAULT,
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        if sys.platform == "win32":
            pref_dir = os.path.join(os.getenv("APPDATA"), "Meld")
        else:
            pref_dir = os.path.join(GLib.get_user_config_dir(), "meld")

        if not os.path.exists(pref_dir):
            os.makedirs(pref_dir)

        self.history_file = os.path.join(pref_dir, "history.ini")
        self.config = configparser.RawConfigParser()
        if os.path.exists(self.history_file):
            self.config.read(self.history_file, encoding='utf8')

        self.set_model(Gtk.ListStore(str, str))
        rentext = Gtk.CellRendererText()
        rentext.props.width_chars = 60
        rentext.props.ellipsize = Pango.EllipsizeMode.END
        self.pack_start(rentext, True)
        self.add_attribute(rentext, 'text', 0)

        self.connect('notify::history-id', lambda *args: self._load_history())
        self.connect('notify::history-length',
                     lambda *args: self._load_history())

    def prepend_history(self, text):
        self._insert_history_item(text, True)

    def append_history(self, text):
        self._insert_history_item(text, False)

    def clear(self):
        self.get_model().clear()
        self._save_history()

    def _insert_history_item(self, text, prepend):
        if not text or len(text) <= MIN_ITEM_LEN:
            return

        store = self.get_model()
        if not _remove_item(store, text):
            _clamp_list_store(store, self.props.history_length - 1)

        row = (text.splitlines()[0], text)

        if prepend:
            store.insert(0, row)
        else:
            store.append(row)
        self._save_history()

    def _load_history(self):
        section_key = self.props.history_id
        if section_key is None or not self.config.has_section(section_key):
            return

        store = self.get_model()
        store.clear()
        messages = sorted(self.config.items(section_key))
        for key, message in messages[:self.props.history_length - 1]:
            message = message.encode('utf8')
            message = message.decode('unicode-escape')
            firstline = message.splitlines()[0]
            store.append((firstline, message))

    def _save_history(self):
        section_key = self.props.history_id
        if section_key is None:
            return

        self.config.remove_section(section_key)
        self.config.add_section(section_key)
        for i, row in enumerate(self.get_model()):
            # This dance is to avoid newline, etc. issues in the ini file
            message = row[1].encode('unicode-escape')
            message = message.decode('utf8')
            self.config.set(section_key, "item%d" % i, message)
        with open(self.history_file, 'w', encoding='utf8') as f:
            self.config.write(f)
Ejemplo n.º 17
0
class ColumnList(Gtk.VBox, EditableListWidget):

    __gtype_name__ = "ColumnList"

    treeview = Gtk.Template.Child()
    remove = Gtk.Template.Child()
    move_up = Gtk.Template.Child()
    move_down = Gtk.Template.Child()

    default_entry = [_("label"), False, _("pattern"), True]

    available_columns = {
        "size": _("Size"),
        "modification time": _("Modification time"),
        "permissions": _("Permissions"),
    }

    settings_key = GObject.Property(
        type=str,
        flags=(GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE
               | GObject.ParamFlags.CONSTRUCT_ONLY),
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.model = self.treeview.get_model()

        # Unwrap the variant
        prefs_columns = [(k, v)
                         for k, v in settings.get_value(self.settings_key)]
        column_vis = {}
        column_order = {}
        for sort_key, (column_name, visibility) in enumerate(prefs_columns):
            column_vis[column_name] = bool(int(visibility))
            column_order[column_name] = sort_key

        columns = [(column_vis.get(name, True), name, label)
                   for name, label in self.available_columns.items()]
        columns = sorted(columns, key=lambda c: column_order.get(c[1], 0))

        for visibility, name, label in columns:
            self.model.append([visibility, name, label])

        for signal in ('row-changed', 'row-deleted', 'row-inserted',
                       'rows-reordered'):
            self.model.connect(signal, self._update_columns)

        self.setup_sensitivity_handling()

    @Gtk.Template.Callback()
    def on_move_up_clicked(self, button):
        self.move_up_selected_entry()

    @Gtk.Template.Callback()
    def on_move_down_clicked(self, button):
        self.move_down_selected_entry()

    @Gtk.Template.Callback()
    def on_cellrenderertoggle_toggled(self, ren, path):
        self.model[path][0] = not ren.get_active()

    def _update_columns(self, *args):
        value = [(c[1].lower(), c[0]) for c in self.model]
        settings.set_value(self.settings_key, GLib.Variant('a(sb)', value))
Ejemplo n.º 18
0
class MainWindow(Gtk.ApplicationWindow):
    network_busy = GObject.Property(type=bool, default=False)

    def __init__(self, application, saved_state):
        self.application = application
        self.saved_state = saved_state
        Gtk.ApplicationWindow.__init__(
            self,
            application=application,
            icon_name="revolt",
            role="main-window",
            default_width=saved_state.get_uint("width"),
            default_height=saved_state.get_uint("height"))
        if self.saved_state.get_boolean("maximized"):
            self.maximize()
        self.saved_state.bind("maximized", self, "is-maximized",
                              Gio.SettingsBindFlags.SET)

        if application.settings.get_boolean("use-header-bar"):
            self.set_titlebar(self.__make_headerbar())
        self.set_title(u"Revolt")
        application.add_window(self)
        self._webview = WebKit2.WebView(
            user_content_manager=self._user_content_manager,
            web_context=self._web_context)
        self._webview.connect("decide-policy", self.__on_decide_policy)
        application.settings.bind("zoom-factor", self._webview, "zoom-level",
                                  Gio.SettingsBindFlags.GET)
        if hasattr(self._webview, "set_maintains_back_forward_list"):
            self._webview.set_maintains_back_forward_list(False)
        websettings = self._webview.get_settings()
        application.settings.bind("enable-developer-tools", websettings,
                                  "enable-developer-extras",
                                  Gio.SettingsBindFlags.GET)
        application.settings.bind("enable-developer-tools", websettings,
                                  "enable-write-console-messages-to-stdout",
                                  Gio.SettingsBindFlags.GET)

        self.add_accel_group(accelerators.window_keys)

        websettings.set_allow_file_access_from_file_urls(True)
        websettings.set_allow_modal_dialogs(False)  # TODO
        websettings.set_enable_fullscreen(False)
        websettings.set_enable_java(False)
        websettings.set_enable_media_stream(True)
        websettings.set_enable_page_cache(False)  # Single-page app
        websettings.set_enable_plugins(False)
        websettings.set_enable_smooth_scrolling(True)
        websettings.set_enable_webaudio(True)
        websettings.set_javascript_can_access_clipboard(True)
        websettings.set_minimum_font_size(12)  # TODO: Make it a setting
        websettings.set_property("enable-mediasource", True)

        # This makes Revolt lighter, and makes things work for people using
        # binary drivers (i.e. NVidia) with Flatpak build. See issue #29.
        if hasattr(websettings, "set_hardware_acceleration_policy"):
            websettings.set_hardware_acceleration_policy(
                WebKit2.HardwareAccelerationPolicy.NEVER)

        self._webview.show_all()
        self.add(self._webview)
        self.__connect_widgets()
        self.__notification_ids = set()

    def do_configure_event(self, event):
        result = Gtk.ApplicationWindow.do_configure_event(self, event)
        width, height = self.get_size()
        self.saved_state.set_uint("width", width)
        self.saved_state.set_uint("height", height)
        return result

    def __make_headerbar(self):
        header = Gtk.HeaderBar()
        header.set_show_close_button(True)
        header.get_style_context().add_class("revolt-slim")
        spinner = Gtk.Spinner()
        header.pack_end(spinner)
        self.bind_property("network-busy", spinner, "active",
                           GObject.BindingFlags.DEFAULT)
        header.show_all()
        return header

    @cachedproperty
    def _website_data_manager(self):
        from os import path as P
        print("Creating WebsiteDataManager...")
        app_id = self.application.get_application_id()
        cache_dir = P.join(GLib.get_user_cache_dir(), "revolt", app_id)
        data_dir = P.join(GLib.get_user_data_dir(), "revolt", app_id)
        return WebKit2.WebsiteDataManager(base_cache_directory=cache_dir,
                                          base_data_directory=data_dir)

    @cachedproperty
    def _web_context(self):
        print("Creating WebContext...")
        ctx = WebKit2.WebContext(
            website_data_manager=self._website_data_manager)
        ctx.set_web_process_count_limit(1)
        ctx.set_spell_checking_enabled(False)
        ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.FAIL)
        return ctx

    @cachedproperty
    def _user_content_manager(self):
        mgr = WebKit2.UserContentManager()
        script = WebKit2.UserScript(
            "Notification.requestPermission();",
            WebKit2.UserContentInjectedFrames.TOP_FRAME,
            WebKit2.UserScriptInjectionTime.START, None, None)
        mgr.add_script(script)
        return mgr

    def __on_decide_policy(self, webview, decision, decision_type):
        if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
            if decision.get_navigation_type(
            ) == WebKit2.NavigationType.LINK_CLICKED:
                uri = decision.get_request().get_uri()
                if not uri.startswith(self.application.riot_url):
                    show_uri(self, uri)
                    return True
        elif decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
            if decision.get_navigation_type(
            ) == WebKit2.NavigationType.LINK_CLICKED:
                show_uri(self, decision.get_request().get_uri())
                return True
        return False

    def __on_has_toplevel_focus_changed(self, window, has_focus):
        assert window == self
        if window.has_toplevel_focus():
            # Clear the window's urgency hint
            window.set_urgency_hint(False)
            # Dismiss notifications
            for notification_id in self.__notification_ids:
                self.application.withdraw_notification(notification_id)
            self.__notification_ids.clear()
            self.application.statusicon.clear_notifications()

    def __on_load_changed(self, webview, event):
        if event == WebKit2.LoadEvent.FINISHED:
            self.network_busy = False
            self.application.statusicon.set_status(statusicon.Status.CONNECTED)
        else:
            self.network_busy = True
            self.application.statusicon.set_status(
                statusicon.Status.DISCONNECTED)

    @cachedproperty
    def _notification_icon(self):
        icon_id = self.application.get_application_id() + "-symbolic"
        return Gio.ThemedIcon.new(icon_id)

    def __on_show_notification(self, webview, notification):
        # TODO: Handle notification clicked, and so
        if not self.has_toplevel_focus():
            self.set_urgency_hint(True)
            notif = Gio.Notification.new(notification.get_title())
            notif.set_body(notification.get_body())
            # TODO: Use the avatar of the contact, if available.
            notif.set_icon(self._notification_icon)
            if not desktop_is("xfce"):  # Workaround for XFCE bug #13586
                notif.set_priority(Gio.NotificationPriority.HIGH)
            # use title as notification id:
            # allows to reuse one notification for the same conversation
            notification_id = notification.get_title()
            self.__notification_ids.add(notification_id)
            self.application.send_notification(notification_id, notif)
            self.application.statusicon.add_notification(
                "%s: %s" % (notification.get_title(), notification.get_body()))
        return True

    def __on_permission_request(self, webview, request):
        if isinstance(request, WebKit2.NotificationPermissionRequest):
            request.allow()
            return True

    def __connect_widgets(self):
        self.connect("notify::has-toplevel-focus",
                     self.__on_has_toplevel_focus_changed)
        self._webview.connect("load-changed", self.__on_load_changed)
        self._webview.connect("show-notification", self.__on_show_notification)
        self._webview.connect("permission-request",
                              self.__on_permission_request)

    def reload_riot(self, bypass_cache=False):
        if bypass_cache:
            self._webview.reload_bypass_cache()
        else:
            self._webview.reload()

    def load_riot(self):
        self._webview.load_uri(self.application.riot_url)
        return self

    def load_settings_page(self):
        from urllib.parse import urlsplit, urlunsplit
        url = list(urlsplit(self._webview.get_uri()))
        url[-1] = "#settings"
        self._webview.load_uri(urlunsplit(url))

    def finish(self):
        # TODO: Most likely this can be moved to do_destroy()
        self._webview.stop_loading()
        self.hide()
        self.destroy()
        del self._webview
        return self
Ejemplo n.º 19
0
class Window(Gtk.ApplicationWindow):

    __gtype_name__ = "Window"

    selected_items_count = GObject.Property(type=int, default=0, minimum=0)
    selection_mode = GObject.Property(type=bool, default=False)

    notifications_popup = Gtk.Template.Child()
    _box = Gtk.Template.Child()
    _overlay = Gtk.Template.Child()
    _selection_toolbar = Gtk.Template.Child()
    _stack = Gtk.Template.Child()

    def __repr__(self):
        return '<Window>'

    @log
    def __init__(self, app):
        """Initialize the main window.

        :param Gtk.Application app: Application object
        """
        super().__init__(application=app, title=_("Music"))

        # Hack
        self._app = app

        self._app._coreselection.bind_property("selected-items-count", self,
                                               "selected-items-count")

        self._settings = app.props.settings
        self.add_action(self._settings.create_action('repeat'))
        select_all = Gio.SimpleAction.new('select_all', None)
        select_all.connect('activate', self._select_all)
        self.add_action(select_all)
        select_none = Gio.SimpleAction.new('select_none', None)
        select_none.connect('activate', self._select_none)
        self.add_action(select_none)

        self.set_size_request(200, 100)
        WindowPlacement(self)

        self.prev_view = None
        self.curr_view = None
        self._view_before_search = None

        self._player = app.props.player

        self._setup_view()

        MediaKeys(self._player, self)

    @log
    def _setup_view(self):
        self._search = Search()
        self._searchbar = SearchBar(self._app)
        self._searchbar.props.stack = self._stack
        self._headerbar_stack = Gtk.Stack()
        transition_type = Gtk.StackTransitionType.CROSSFADE
        self._headerbar_stack.props.transition_type = transition_type
        self._headerbar = HeaderBar()
        self._search_headerbar = SearchHeaderBar(self._app)
        self._search_headerbar.props.stack = self._stack
        self._headerbar_stack.add_named(self._headerbar, "main")
        self._headerbar_stack.add_named(self._search_headerbar, "search")
        self._headerbar_stack.props.name = "search"

        self._search.bind_property(
            "search-mode-active", self._headerbar, "search-mode-active",
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)
        self._search.bind_property("search-mode-active", self._searchbar,
                                   "search-mode-enabled",
                                   GObject.BindingFlags.SYNC_CREATE)
        self._search.bind_property(
            "search-mode-active", self._search_headerbar, "search-mode-active",
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)
        self._search.bind_property("state", self._searchbar, "search-state",
                                   GObject.BindingFlags.SYNC_CREATE)

        self._search.connect("notify::search-mode-active",
                             self._on_search_mode_changed)

        self._player_toolbar = PlayerToolbar()
        self._player_toolbar.props.player = self._player

        self._headerbar.connect('back-button-clicked',
                                self._switch_back_from_childview)

        self.bind_property('selected-items-count', self._headerbar,
                           'selected-items-count')
        self.bind_property("selected-items-count", self._selection_toolbar,
                           "selected-items-count")
        self.bind_property(
            'selection-mode', self._headerbar, 'selection-mode',
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)
        self.bind_property("selected-items-count", self._search_headerbar,
                           "selected-items-count")
        self.bind_property(
            "selection-mode", self._search_headerbar, "selection-mode",
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)
        self.bind_property("selection-mode", self._player_toolbar, "visible",
                           GObject.BindingFlags.INVERT_BOOLEAN)
        self.bind_property("selection-mode", self._selection_toolbar,
                           "visible")
        self.connect("notify::selection-mode", self._on_selection_mode_changed)

        self.views = [Gtk.Box()] * len(View)
        # Create only the empty view at startup
        # if no music, switch to empty view and hide stack
        # if some music is available, populate stack with mainviews,
        # show stack and set empty_view to empty_search_view
        self.views[View.EMPTY] = EmptyView()
        self._stack.add_named(self.views[View.EMPTY], "emptyview")

        # Add the 'background' styleclass so it properly hides the
        # bottom line of the searchbar
        self._stack.get_style_context().add_class('background')

        # FIXME: Need to find a proper way to do this.
        # self._overlay.add_overlay(self._searchbar._dropdown)

        # self._box.pack_start(self._searchbar, False, False, 0)
        # self._box.reorder_child(self._searchbar, 0)
        self._box.pack_end(self._player_toolbar, False, False, 0)

        self.set_titlebar(self._headerbar_stack)

        self._selection_toolbar.connect('add-to-playlist',
                                        self._on_add_to_playlist)
        self._search.connect("notify::state", self._on_search_state_changed)

        self._headerbar.props.state = HeaderBar.State.MAIN
        self._headerbar_stack.show_all()

        self._app.props.coremodel.connect("notify::songs-available",
                                          self._on_songs_available)

        self._app.props.coremodel.props.grilo.connect(
            "notify::tracker-available", self._on_tracker_available)

        if self._app.props.coremodel.props.songs_available:
            self._switch_to_player_view()
        else:
            self._switch_to_empty_view()

    @log
    def _switch_to_empty_view(self):
        did_initial_state = self._settings.get_boolean('did-initial-state')

        state = self._app.props.coremodel.props.grilo.props.tracker_available
        empty_view = self.views[View.EMPTY]
        if state == TrackerState.UNAVAILABLE:
            empty_view.props.state = EmptyView.State.NO_TRACKER
        elif state == TrackerState.OUTDATED:
            empty_view.props.state = EmptyView.State.TRACKER_OUTDATED
        elif did_initial_state:
            empty_view.props.state = EmptyView.State.EMPTY
        else:
            # FIXME: On switch back this view does not show properly.
            empty_view.props.state = EmptyView.State.INITIAL

        self._headerbar.props.state = HeaderBar.State.EMPTY

    def _on_search_mode_changed(self, search, value):
        if self._search.props.search_mode_active:
            self._headerbar_stack.set_visible_child_name("search")
        else:
            self._headerbar_stack.set_visible_child_name("main")

    def _on_songs_available(self, klass, value):
        if self._app.props.coremodel.props.songs_available:
            self._switch_to_player_view()
        else:
            self._switch_to_empty_view()

    def _on_tracker_available(self, klass, value):
        grilo = self._app.props.coremodel.props.grilo
        new_state = grilo.props.tracker_available

        if new_state != TrackerState.AVAILABLE:
            self._switch_to_empty_view()

        self._on_songs_available(None, None)

    @log
    def _switch_to_player_view(self):
        self._settings.set_boolean('did-initial-state', True)
        self._on_notify_model_id = self._stack.connect('notify::visible-child',
                                                       self._on_notify_mode)
        self.connect('destroy', self._notify_mode_disconnect)
        self._key_press_event_id = self.connect('key_press_event',
                                                self._on_key_press)

        self._btn_ctrl = Gtk.GestureMultiPress().new(self)
        self._btn_ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
        # Mouse button 8 is the back button.
        self._btn_ctrl.props.button = 8
        self._btn_ctrl.connect("pressed", self._on_back_button_pressed)

        self.views[View.EMPTY].props.state = EmptyView.State.SEARCH

        # FIXME: In case Grilo is already initialized before the views
        # get created, they never receive a 'ready' signal to trigger
        # population. To fix this another check was added to baseview
        # to populate if grilo is ready at the end of init. For this to
        # work however, the headerbar stack needs to be created and
        # populated. This is done below, by binding headerbar.stack to
        # to window._stack. For this to succeed, the stack needs to be
        # filled with something: Gtk.Box.
        # This is a bit of circular logic that needs to be fixed.
        self._headerbar.props.state = HeaderBar.State.MAIN
        self._headerbar.props.stack = self._stack
        # self._searchbar.show()

        self.views[View.ALBUM] = AlbumsView(self._app, self._player)
        self.views[View.ARTIST] = ArtistsView(self._app, self._player)
        self.views[View.SONG] = SongsView(self._app, self._player)
        self.views[View.PLAYLIST] = PlaylistsView(self._app, self._player)
        self.views[View.SEARCH] = SearchView(self._app, self._player)

        selectable_views = [View.ALBUM, View.ARTIST, View.SONG, View.SEARCH]
        for view in selectable_views:
            self.views[view].bind_property('selected-items-count', self,
                                           'selected-items-count')

        # empty view has already been created in self._setup_view starting at
        # View.ALBUM
        # empty view state is changed once album view is visible to prevent it
        # from being displayed during startup
        for i in self.views[View.ALBUM:]:
            if i.title:
                self._stack.add_titled(i, i.name, i.title)
            else:
                self._stack.add_named(i, i.name)

        self._stack.set_visible_child(self.views[View.ALBUM])

        self.views[View.SEARCH].bind_property("search-state", self._search,
                                              "state",
                                              GObject.BindingFlags.SYNC_CREATE)
        self._search.bind_property("search-mode-active",
                                   self.views[View.SEARCH],
                                   "search-mode-active",
                                   GObject.BindingFlags.BIDIRECTIONAL)
        self._search.bind_property("search-mode-active",
                                   self.views[View.ALBUM],
                                   "search-mode-active",
                                   GObject.BindingFlags.SYNC_CREATE)

    @log
    def _select_all(self, action=None, param=None):
        if not self.props.selection_mode:
            return
        if self._headerbar.props.state == HeaderBar.State.MAIN:
            view = self._stack.get_visible_child()
        else:
            view = self._stack.get_visible_child().get_visible_child()

        view.select_all()

    @log
    def _select_none(self, action=None, param=None):
        if not self.props.selection_mode:
            return
        if self._headerbar.props.state == HeaderBar.State.MAIN:
            view = self._stack.get_visible_child()
            view.unselect_all()
        else:
            view = self._stack.get_visible_child().get_visible_child()
            view.select_none()

    @log
    def _on_key_press(self, widget, event):
        modifiers = event.get_state() & Gtk.accelerator_get_default_mod_mask()
        (_, keyval) = event.get_keyval()

        control_mask = Gdk.ModifierType.CONTROL_MASK
        shift_mask = Gdk.ModifierType.SHIFT_MASK
        mod1_mask = Gdk.ModifierType.MOD1_MASK
        shift_ctrl_mask = control_mask | shift_mask

        # Ctrl+<KEY>
        if control_mask == modifiers:
            if keyval == Gdk.KEY_a:
                self._select_all()
            # Open search bar on Ctrl + F
            if (keyval == Gdk.KEY_f
                    and not self.views[View.PLAYLIST].rename_active
                    and self._headerbar.props.state != HeaderBar.State.SEARCH):
                search_mode = self._search.props.search_mode_active
                self._search.props.search_mode_active = not search_mode
            # Play / Pause on Ctrl + SPACE
            if keyval == Gdk.KEY_space:
                self._player.play_pause()
            # Play previous on Ctrl + B
            if keyval == Gdk.KEY_b:
                self._player.previous()
            # Play next on Ctrl + N
            if keyval == Gdk.KEY_n:
                self._player.next()
            if keyval == Gdk.KEY_q:
                self.props.application.quit()
            # Toggle repeat on Ctrl + R
            if keyval == Gdk.KEY_r:
                if self._player.props.repeat_mode == RepeatMode.SONG:
                    self._player.props.repeat_mode = RepeatMode.NONE
                    repeat_state = GLib.Variant("s", ("none"))
                else:
                    self._player.props.repeat_mode = RepeatMode.SONG
                    repeat_state = GLib.Variant("s", ("song"))
                self.lookup_action('repeat').change_state(repeat_state)
            # Toggle shuffle on Ctrl + S
            if keyval == Gdk.KEY_s:
                if self._player.props.repeat_mode == RepeatMode.SHUFFLE:
                    self._player.props.repeat_mode = RepeatMode.NONE
                    repeat_state = GLib.Variant("s", ("none"))
                else:
                    self._player.props.repeat_mode = RepeatMode.SHUFFLE
                    repeat_state = GLib.Variant("s", ("shuffle"))
                self.lookup_action('repeat').change_state(repeat_state)
        # Ctrl+Shift+<KEY>
        elif modifiers == shift_ctrl_mask:
            if keyval == Gdk.KEY_A:
                self._select_none()
        # Alt+<KEY>
        elif modifiers == mod1_mask:
            # Go back from child view on Alt + Left
            if keyval == Gdk.KEY_Left:
                self._switch_back_from_childview()
            # Headerbar switching
            if keyval in [Gdk.KEY_1, Gdk.KEY_KP_1]:
                self._toggle_view(View.ALBUM)
            if keyval in [Gdk.KEY_2, Gdk.KEY_KP_2]:
                self._toggle_view(View.ARTIST)
            if keyval in [Gdk.KEY_3, Gdk.KEY_KP_3]:
                self._toggle_view(View.SONG)
            if keyval in [Gdk.KEY_4, Gdk.KEY_KP_4]:
                self._toggle_view(View.PLAYLIST)
        # No modifier
        else:
            if (keyval == Gdk.KEY_AudioPlay or keyval == Gdk.KEY_AudioPause):
                self._player.play_pause()

            if keyval == Gdk.KEY_AudioStop:
                self._player.stop()

            if keyval == Gdk.KEY_AudioPrev:
                self._player.previous()

            if keyval == Gdk.KEY_AudioNext:
                self._player.next()

            child = self._stack.get_visible_child()
            if (keyval == Gdk.KEY_Delete
                    and child == self.views[View.PLAYLIST]):
                self.views[View.PLAYLIST].remove_playlist()
            # Close selection mode or search bar after Esc is pressed
            if keyval == Gdk.KEY_Escape:
                if self.props.selection_mode:
                    self.props.selection_mode = False
                elif self._search.props.search_mode_active:
                    self._search.props.search_mode_active = False

        # Open the search bar when typing printable chars.
        key_unic = Gdk.keyval_to_unicode(keyval)
        if ((not self._search.props.search_mode_active
             and not keyval == Gdk.KEY_space)
                and GLib.unichar_isprint(chr(key_unic))
                and (modifiers == shift_mask or modifiers == 0)
                and not self.views[View.PLAYLIST].rename_active
                and self._headerbar.props.state != HeaderBar.State.SEARCH):
            self._search.props.search_mode_active = True

    @log
    def _on_back_button_pressed(self, gesture, n_press, x, y):
        self._headerbar.emit('back-button-clicked')

    @log
    def _notify_mode_disconnect(self, data=None):
        self._player.stop()
        self.notifications_popup.terminate_pending()
        self._stack.disconnect(self._on_notify_model_id)

    @log
    def _on_notify_mode(self, stack, param):
        self.prev_view = self.curr_view
        self.curr_view = stack.get_visible_child()

        # Disable search mode when switching view
        search_views = [self.views[View.EMPTY], self.views[View.SEARCH]]
        if (self.curr_view in search_views
                and self.prev_view not in search_views):
            self._view_before_search = self.prev_view
        elif (self.curr_view not in search_views
              and self._search.props.search_mode_active is True):
            self._search.props.search_mode_active = False

        # Disable the selection button for the EmptySearch and Playlist
        # view
        no_selection_mode = [self.views[View.EMPTY], self.views[View.PLAYLIST]]
        allowed = self.curr_view not in no_selection_mode
        self._headerbar.props.selection_mode_allowed = allowed

        # Disable renaming playlist if it was active when leaving
        # Playlist view
        if (self.prev_view == self.views[View.PLAYLIST]
                and self.views[View.PLAYLIST].rename_active):
            self.views[View.PLAYLIST].disable_rename_playlist()

    @log
    def _toggle_view(self, view_enum):
        # TODO: The SEARCH state actually refers to the child state of
        # the search mode. This fixes the behaviour as needed, but is
        # incorrect: searchview currently does not switch states
        # correctly.
        if (not self.props.selection_mode
                and not self._headerbar.props.state == HeaderBar.State.CHILD
                and not self._headerbar.props.state == HeaderBar.State.SEARCH):
            self._stack.set_visible_child(self.views[view_enum])

    @log
    def _on_search_state_changed(self, klass, param):
        if (self._search.props.state != Search.State.NONE
                or not self._view_before_search):
            return

        # Get back to the view before the search
        self._stack.set_visible_child(self._view_before_search)

    @log
    def _switch_back_from_childview(self, klass=None):
        if self.props.selection_mode:
            return

        views_with_child = [self.views[View.ALBUM], self.views[View.SEARCH]]
        if self.curr_view in views_with_child:
            self.curr_view._back_button_clicked(self.curr_view)

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        if (not self.props.selection_mode
                and self._player.state == Playback.STOPPED):
            self._player_toolbar.hide()

    @log
    def _on_add_to_playlist(self, widget):
        if self._stack.get_visible_child() == self.views[View.PLAYLIST]:
            return

        selected_songs = self._app._coreselection.props.selected_items

        if len(selected_songs) < 1:
            return

        playlist_dialog = PlaylistDialog(self)
        if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
            playlist = playlist_dialog.props.selected_playlist
            playlist.add_songs(selected_songs)

        self.props.selection_mode = False
        playlist_dialog.destroy()

    @log
    def set_player_visible(self, visible):
        """Set PlayWidget action visibility

        :param bool visible: actionbar visibility
        """
        self._player_toolbar.set_visible(visible)
Ejemplo n.º 20
0
class Application(Gtk.Application):
    module = GObject.Property(type=GHandle,
                              default=lb.get_null_handle(),
                              nick='module',
                              blurb='Handle to the LLVM module')

    llvm = GObject.Property(
        type=str,
        default='',
        nick='llvm',
        blurb='The path to the LLVM file currently shown or ""')

    entity = GObject.Property(type=GHandle,
                              default=lb.get_null_handle(),
                              nick='entity',
                              blurb=('The current entity. '
                                     'This will be a use, def or comdat'))

    inst = GObject.Property(type=GHandle,
                            default=lb.get_null_handle(),
                            nick='inst',
                            blurb=('The current instruction'))

    func = GObject.Property(type=GHandle,
                            default=lb.get_null_handle(),
                            nick='func',
                            blurb=('The current function. '
                                   'This may be set even if self.inst is not'))

    # The entity with source is used to enable the "view source" action
    # Both the current instruction and the current function could have source
    # information associated with it. In that case, we give the instruction
    # source priority if it is set. There may be some instructions in the
    # function that don't have source infromation associated with it
    # These could be LLVM intrinsic instructions for instance. In such case,
    # the function should be used as the source entity
    entity_with_source = GObject.Property(
        type=GHandle,
        default=lb.get_null_handle(),
        nick='entity-with-source',
        blurb=('Handle of the entity whose source will be shown '
               'when the show-source action is launched.'))

    # entity_with_def will be the same as self.entity
    # if self.entity is a use or a comdat. Else it will be None
    entity_with_def = GObject.Property(
        type=GHandle,
        default=lb.get_null_handle(),
        nick='entity-with-def',
        blurb=('Handle of the entity with a definition. '
               'Used when the goto definition action is launched'))

    mark = GObject.Property(
        type=GHandle,
        default=lb.get_null_handle(),
        nick='curr-mark',
        blurb=('Handle of the marked entity. '
               'This is the handle at the top of the self.marks stack'))

    mark_offset = GObject.Property(type=GObject.TYPE_UINT64,
                                   default=0,
                                   nick='mark-offset',
                                   blurb='Offset of the mark in the LLVM IR')

    mark_uses_count = GObject.Property(
        type=int,
        default=0,
        nick='mark-uses-count',
        blurb=('The number of uses for the currently marked entity'))

    mark_uses_index = GObject.Property(
        type=int,
        default=-1,
        nick='mark-uses-index',
        blurb=('The index into the use list for the currently marked entity. '
               'This is the count at the top of the self.marks stack'))

    def __init__(self):
        Gtk.Application.__init__(self)

        GLib.set_application_name('LLVM Browse')
        GLib.set_prgname('llvm-browse')

        self.argv: argparse.Namespace = None
        self.options: Options = Options(self)
        self.ui: UI = UI(self)

        # The user has to explicitly set a mark. When one is set, prev-use
        # and next-use will be enabled and the user can navigate this list
        self.marks: List[Tuple[int, int]] = []

        # A map from entities to uses. The keys are all guaranteed to be in
        # self.marks
        self.uses_map: Mapping[int, List[int]] = {}

        # Map from the entities with marks set to the index of the use that
        # was last jumped to using prev-us or next-use. Because the same
        # entity can be marked more than once, the value is a list
        self.uses_indexes_map: Mapping[int, List[int]] = {}

        self.connect('notify::entity', self.on_entity_changed)
        self.connect('notify::inst', self.on_instruction_changed)
        self.connect('notify::func', self.on_function_changed)

    def _reset(self):
        if self.module:
            lb.module_free(self.module)
        self.module = lb.get_null_handle()
        self.llvm = ''

    def do_activate(self, *args) -> bool:
        self.options.load()
        self.add_window(self.ui.get_application_window())
        self.ui.emit('launch')
        if self.argv.maximize:
            self.ui.win_main.maximize()
        if self.argv.file:
            self.action_open(self.argv.file)
        return False

    # Returns true if the file could be opened
    def action_open(self, file: str) -> bool:
        self.llvm = file
        self.module = lb.module_create(file)
        if not self.module:
            self._reset()
        else:
            self.ui.do_open()
        return bool(self.module)

    # Returns true if the file could be closed
    def action_close(self) -> bool:
        self._reset()
        return True

    # Returns true if the file could be reloaded
    def action_reload(self) -> bool:
        if self.llvm:
            llvm = self.llvm
            self._reset()
            return self.action_open(llvm)
        return False

    # Returns true on success. Not sure if this will actually return
    def action_quit(self) -> bool:
        self._reset()
        self.remove_window(self.ui.get_application_window())
        return True

    def action_goto_definition(self) -> bool:
        if self.entity_with_def:
            defn = lb.entity_get_llvm_defn(self.entity_with_def)
            offset = lb.def_get_begin(defn)
            tag = lb.entity_get_tag(self.entity_with_def)
            self.ui.do_scroll_llvm_to_offset(offset, len(tag))
            return True
        return False

    def action_goto_prev_use(self) -> bool:
        if self.marks:
            if self.mark_uses_count:
                # If there is only a single use, the "previous use" is the same
                # as the only use even if we are already at that single use
                if self.mark_uses_count > 1:
                    if self.mark_uses_index == 0:
                        self.mark_uses_index = self.mark_uses_count
                    self.mark_uses_index -= 1
                use = self.uses_map[self.mark][self.mark_uses_index]
                tag = lb.entity_get_tag(lb.use_get_used(use))
                self.ui.do_scroll_llvm_to_offset(lb.use_get_begin(use),
                                                 len(tag))
                return True
            return False
        return False

    def action_goto_next_use(self) -> bool:
        if self.marks:
            if self.mark_uses_count:
                # If there is only a single use, the "next use" is the same
                # as the only use even if we are already at that single use
                if self.mark_uses_count > 1:
                    if self.mark_uses_index == self.mark_uses_count - 1:
                        self.mark_uses_index = -1
                    self.mark_uses_index += 1
                use = self.uses_map[self.mark][self.mark_uses_index]
                tag = lb.entity_get_tag(lb.use_get_used(use))
                self.ui.do_scroll_llvm_to_offset(lb.use_get_begin(use),
                                                 len(tag))
            return False
        return False

    def action_goto_prev_mark(self) -> bool:
        pass

    def action_goto_next_mark(self) -> bool:
        pass

    def action_show_source(self) -> bool:
        if self.entity_with_source:
            self.ui.do_show_source(self.entity_with_source)
            return True
        return False

    def set_mark(self, entity: int, offset: int = 0):
        if lb.is_null_handle(entity):
            self.mark = lb.get_null_handle()
            self.mark_offset = offset
            self.mark_uses_count = 0
            self.mark_uses_index = -1
        else:
            self.mark = entity
            self.mark_offset = offset
            self.mark_uses_count = len(self.uses_map[entity])
            self.mark_uses_index = self.uses_indexes_map[entity][-1]

    def action_mark_push(self, entity: int, offset: int) -> bool:
        if len(self.marks) == self.options.max_marks:
            return False

        if lb.is_null_handle(entity):
            # If the entity being pushed is not a use, add a mark anyway
            # This mark will be used for navigation only
            self.marks.append((lb.get_null_handle(), offset))
        else:
            uses = []
            if lb.is_use(entity):
                uses = lb.entity_get_uses(lb.use_get_used(entity))
            elif lb.is_def(entity):
                uses = lb.entity_get_uses(lb.def_get_defined(entity))
            elif lb.is_comdat(entity):
                uses = [lb.comdat_get_target(entity)]
            self.marks.append((entity, offset))
            if entity not in self.uses_indexes_map:
                self.uses_map[entity] = uses
                self.uses_indexes_map[entity] = []
            # FIXME: A better way to do this would be to set it to the use
            # nearest the offset
            self.uses_indexes_map[entity].append(0 if uses else -1)
        self.set_mark(entity, offset)
        return True

    def action_mark_pop(self) -> bool:
        if not self.marks:
            return False

        # If there is at least one mark, pop it
        if self.marks:
            entity, _ = self.marks.pop()
            # If the mark is actually a use, then remove all the metadata
            # associated with it if it is the last instance of the mark
            if not lb.is_null_handle(entity):
                self.uses_indexes_map[entity].pop()
                if len(self.uses_indexes_map[entity]) == 0:
                    del self.uses_indexes_map[entity]
                    del self.uses_map[entity]
        if self.marks:
            self.set_mark(self.marks[-1][0], self.marks[-1][1])
            # FIXME: When a mark is popped, move the cursor to the last seen use
            # of the previous mark if any
        else:
            self.set_mark(lb.get_null_handle())

        return True

    def on_entity_changed(self, *args):
        self.entity_with_def = lb.get_null_handle()
        if self.entity:
            if lb.is_use(self.entity):
                used = lb.use_get_used(self.entity)
                if lb.entity_has_llvm_defn(used):
                    self.entity_with_def = used
            elif lb.is_def(self.entity):
                self.entity_with_def = lb.get_null_handle()
            elif lb.is_comdat(self.entity):
                self.entity_with_def = self.entity

    def on_instruction_changed(self, *args):
        def has_source(inst):
            if lb.inst_has_source_defn(inst):
                # We don't want to allow "show-source" for instructions with
                # LLVM's debug or lifetime intrinsics. These can't really be
                # mapped to anything in the source. In any case, the "useful"
                # information in those instructions is the LLVM value actually
                # passed to the call, so no point in confusing matters by
                # allowing it to be mapped to the source
                if lb.inst_is_llvm_debug_inst(inst) \
                        or lb.inst_is_llvm_lifetime_inst(inst):
                    return False
                return True
            return False

        # If an instruction was set and it has source information, then
        # set the source entity to be that instruction. If not, then
        # set it to the containing function
        self.entity_with_source = lb.get_null_handle()
        if self.inst:
            self.func = lb.inst_get_function(self.inst)
            if has_source(self.inst):
                self.entity_with_source = self.inst
            elif lb.func_has_source_defn(self.func):
                self.entity_with_source = self.func

    def on_function_changed(self, *args):
        # If a function was set, then check if an instruction has also been
        # set. If the instruction has not been set, then set the source entity
        # to be the function if it has source information
        if self.func:
            if (not self.inst) and lb.func_has_source_defn(self.func):
                self.entity_with_source = self.func

    def run(self, argv: argparse.Namespace) -> int:
        self.argv = argv
        ret = Gtk.Application.run(self)
        self.options.store()
        return ret
Ejemplo n.º 21
0
class FindBar(Gtk.Grid):

    __gtype_name__ = 'FindBar'

    find_entry = Template.Child()
    find_next_button = Template.Child()
    find_previous_button = Template.Child()
    match_case = Template.Child()
    regex = Template.Child()
    replace_all_button = Template.Child()
    replace_button = Template.Child()
    replace_entry = Template.Child()
    whole_word = Template.Child()
    wrap_box = Template.Child()

    replace_mode = GObject.Property(type=bool, default=False)

    @GObject.Signal(
        name='activate-secondary',
        flags=(
            GObject.SignalFlags.RUN_FIRST |
            GObject.SignalFlags.ACTION
        ),
    )
    def activate_secondary(self) -> None:
        self._find_text(backwards=True)

    def __init__(self, parent):
        super().__init__()
        self.init_template()

        self.search_context = None
        self.notify_id = None
        self.set_text_view(None)

        # Setup a signal for when the find bar loses focus
        parent.connect('set-focus-child', self.on_focus_child)

        # Create and bind our GtkSourceSearchSettings
        settings = GtkSource.SearchSettings()
        self.match_case.bind_property('active', settings, 'case-sensitive')
        self.whole_word.bind_property('active', settings, 'at-word-boundaries')
        self.regex.bind_property('active', settings, 'regex-enabled')
        self.find_entry.bind_property('text', settings, 'search-text')
        settings.set_wrap_around(True)
        self.search_settings = settings

        # Bind visibility and layout for find-and-replace mode
        self.bind_property('replace_mode', self.replace_entry, 'visible')
        self.bind_property('replace_mode', self.replace_all_button, 'visible')
        self.bind_property('replace_mode', self.replace_button, 'visible')
        self.bind_property(
            'replace_mode', self, 'row-spacing', GObject.BindingFlags.DEFAULT,
            lambda binding, replace_mode: 6 if replace_mode else 0)

    def on_focus_child(self, container, widget):
        if widget is not None:
            visible = self.props.visible
            if widget is not self and visible:
                self.hide()
        return False

    def hide(self):
        self.set_text_view(None)
        self.wrap_box.set_visible(False)
        Gtk.Widget.hide(self)

    def update_match_state(self, *args):
        # Note that -1 here implies that the search is still running
        no_matches = (
            self.search_context.props.occurrences_count == 0 and
            self.search_settings.props.search_text
        )
        style_context = self.find_entry.get_style_context()
        if no_matches:
            style_context.add_class(Gtk.STYLE_CLASS_ERROR)
        else:
            style_context.remove_class(Gtk.STYLE_CLASS_ERROR)

    def set_text_view(self, textview):
        self.textview = textview
        if textview is not None:
            self.search_context = GtkSource.SearchContext.new(
                textview.get_buffer(), self.search_settings)
            self.search_context.set_highlight(True)
            self.notify_id = self.search_context.connect(
                'notify::occurrences-count', self.update_match_state)
        else:
            if self.notify_id:
                self.search_context.disconnect(self.notify_id)
                self.notify_id = None
            self.search_context = None

    def start_find(self, *, textview: Gtk.TextView, replace: bool, text: str):
        self.replace_mode = replace
        self.set_text_view(textview)
        if text:
            self.find_entry.set_text(text)
        self.show()
        self.find_entry.grab_focus()

    def start_find_next(self, textview):
        self.set_text_view(textview)
        self._find_text()

    def start_find_previous(self, textview):
        self.set_text_view(textview)
        self._find_text(backwards=True)

    @Template.Callback()
    def on_find_next_button_clicked(self, button):
        self._find_text()

    @Template.Callback()
    def on_find_previous_button_clicked(self, button):
        self._find_text(backwards=True)

    @Template.Callback()
    def on_replace_button_clicked(self, entry):
        buf = self.textview.get_buffer()
        oldsel = buf.get_selection_bounds()
        match = self._find_text(0)
        newsel = buf.get_selection_bounds()
        # Only replace if there is an already-selected match at the cursor
        if (match and oldsel and oldsel[0].equal(newsel[0]) and
                oldsel[1].equal(newsel[1])):
            self.search_context.replace(
                newsel[0], newsel[1], self.replace_entry.get_text(), -1)
            self._find_text(0)

    @Template.Callback()
    def on_replace_all_button_clicked(self, entry):
        buf = self.textview.get_buffer()
        saved_insert = buf.create_mark(
            None, buf.get_iter_at_mark(buf.get_insert()), True)
        self.search_context.replace_all(self.replace_entry.get_text(), -1)
        if not saved_insert.get_deleted():
            buf.place_cursor(buf.get_iter_at_mark(saved_insert))
            self.textview.scroll_to_mark(
                buf.get_insert(), 0.25, True, 0.5, 0.5)

    @Template.Callback()
    def on_toggle_replace_button_clicked(self, button):
        self.replace_mode = not self.replace_mode

    @Template.Callback()
    def on_find_entry_changed(self, entry):
        self._find_text(0)

    @Template.Callback()
    def on_stop_search(self, search_entry):
        self.hide()

    def _find_text(self, start_offset=1, backwards=False):
        if not self.textview or not self.search_context:
            return

        buf = self.textview.get_buffer()
        insert = buf.get_iter_at_mark(buf.get_insert())

        start, end = buf.get_bounds()
        self.wrap_box.set_visible(False)
        if not backwards:
            insert.forward_chars(start_offset)
            match, start, end, wrapped = self.search_context.forward(insert)
        else:
            match, start, end, wrapped = self.search_context.backward(insert)

        if match:
            self.wrap_box.set_visible(wrapped)
            buf.place_cursor(start)
            buf.move_mark(buf.get_selection_bound(), end)
            self.textview.scroll_to_mark(
                buf.get_insert(), 0.25, True, 0.5, 0.5)
            return True
        else:
            buf.place_cursor(buf.get_iter_at_mark(buf.get_insert()))
            self.wrap_box.set_visible(False)
Ejemplo n.º 22
0
class DuplicateLinePlugin(GObject.Object, Peas.Activatable):
    __gtype_name__ = 'DuplicateLinePlugin'

    object = GObject.Property(type=GObject.Object)

    def __init__(self):
        super().__init__()

    def do_activate(self):
        self.window = self.object
        manager = self.window.get_ui_manager()

        action = Gtk.Action.new('DuplicateLine', _('Duplicate Line/Selection'))
        action.connect('activate', lambda a: self.duplicate_line())

        self.action_group = Gtk.ActionGroup.new('DuplicateLinePluginActions')
        self.action_group.add_action_with_accel(action, '<Ctrl><Shift>d')

        manager.insert_action_group(self.action_group, -1)
        self.merge_id = manager.add_ui_from_string(ui_str)

    def do_deactivate(self):
        manager = self.window.get_ui_manager()
        manager.remove_ui(self.merge_id)
        manager.remove_action_group(self.action_group)
        manager.ensure_update()

    def do_update_state(self):
        pass

    def duplicate_line(self):
        doc = self.window.get_active_document()
        if doc is None:
            return

        if doc.get_has_selection():
            start, end = doc.get_selection_bounds()
            if start.get_line() != end.get_line():
                start.set_line_offset(0)
                if not end.ends_line():
                    end.forward_to_line_end()

                lines = doc.get_text(start, end, False)
                if lines[-1] != '\n':
                    lines = f'\n{lines}'

                doc.insert(end, lines)
            else:
                selection = doc.get_text(start, end, False)

                doc.move_mark_by_name('selection_bound', start)
                doc.insert(end, selection)
        else:
            start = doc.get_iter_at_mark(doc.get_insert())
            start.set_line_offset(0)

            end = start.copy()
            if not end.ends_line():
                end.forward_to_line_end()

            curr_line = doc.get_text(start, end, False)
            doc.insert(end, f'\n{curr_line}')
Ejemplo n.º 23
0
class DiscBox(Gtk.Box):
    """A widget which compromises one disc

    DiscBox contains a disc label for the disc number on top
    with a DiscSongsFlowBox beneath.
    """
    __gtype_name__ = 'DiscBox'

    _disc_label = Gtk.Template.Child()
    _disc_songs_flowbox = Gtk.Template.Child()

    __gsignals__ = {
        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-activated': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,))
    }

    columns = GObject.Property(type=int, minimum=1, default=1)
    selection_mode = GObject.Property(type=bool, default=False)
    selection_mode_allowed = GObject.Property(type=bool, default=True)
    show_disc_label = GObject.Property(type=bool, default=False)
    show_durations = GObject.Property(type=bool, default=False)
    show_favorites = GObject.Property(type=bool, default=False)
    show_song_numbers = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<DiscBox>'

    @log
    def __init__(self, model=None):
        """Initialize

        :param model: The TreeStore to use
        """
        super().__init__()

        self._model = model
        self._model.connect('row-changed', self._model_row_changed)

        self.bind_property(
            'columns', self._disc_songs_flowbox, 'columns',
            GObject.BindingFlags.SYNC_CREATE)
        self.bind_property(
            'show-disc-label', self._disc_label, 'visible',
            GObject.BindingFlags.SYNC_CREATE)

        self._selection_mode_allowed = True
        self._selected_items = []
        self._songs = []

    @log
    def set_disc_number(self, disc_number):
        """Set the dics number to display

        :param int disc_number: Disc number to display
        """
        self._disc_label.props.label = _("Disc {}").format(disc_number)
        self._disc_label.props.visible = True

    @log
    def set_songs(self, songs):
        """Songs to display

        :param list songs: A list of Grilo media items to
        add to the widget
        """
        for song in songs:
            song_widget = self._create_song_widget(song)
            self._disc_songs_flowbox.insert(song_widget, -1)
            song.song_widget = song_widget

    @log
    def get_selected_items(self):
        """Return all selected items

        :returns: The selected items:
        :rtype: A list if Grilo media items
        """
        self._selected_items = []
        self._disc_songs_flowbox.foreach(self._get_selected)

        return self._selected_items

    @log
    def _get_selected(self, child):
        song_widget = child.get_child()

        if song_widget.selected:
            itr = song_widget.itr
            self._selected_items.append(self._model[itr][5])

    # FIXME: select all/none slow probably b/c of the row changes
    # invocations, maybe workaround?
    @log
    def select_all(self):
        """Select all songs"""
        def child_select_all(child):
            song_widget = child.get_child()
            self._model[song_widget.itr][6] = True

        self._disc_songs_flowbox.foreach(child_select_all)

    @log
    def select_none(self):
        """Deselect all songs"""
        def child_select_none(child):
            song_widget = child.get_child()
            self._model[song_widget.itr][6] = False

        self._disc_songs_flowbox.foreach(child_select_none)

    @log
    def _create_song_widget(self, song):
        """Helper function to create a song widget for a
        single song

        :param song: A Grilo media item
        :returns: A complete song widget
        :rtype: Gtk.EventBox
        """
        song_widget = SongWidget(song)
        self._songs.append(song_widget)

        title = utils.get_media_title(song)

        itr = self._model.append(None)

        self._model[itr][0, 1, 2, 5, 6] = [title, '', '', song, False]

        song_widget.itr = itr
        song_widget.model = self._model
        song_widget.connect('button-release-event', self._song_activated)
        song_widget.connect('selection-changed', self._on_selection_changed)

        self.bind_property(
            'selection-mode', song_widget, 'selection-mode',
            GObject.BindingFlags.SYNC_CREATE)
        self.bind_property(
            'show-durations', song_widget, 'show-duration',
            GObject.BindingFlags.SYNC_CREATE)
        self.bind_property(
            'show-favorites', song_widget, 'show-favorite',
            GObject.BindingFlags.SYNC_CREATE)
        self.bind_property(
            'show-song-numbers', song_widget, 'show-song-number',
            GObject.BindingFlags.SYNC_CREATE)

        return song_widget

    @log
    def _on_selection_changed(self, widget):
        self.emit('selection-changed')

        return True

    @log
    def _toggle_widget_selection(self, child):
        song_widget = child.get_child()
        song_widget.props.selection_mode = self.props.selection_mode

    @log
    def _song_activated(self, widget, event):
        mod_mask = Gtk.accelerator_get_default_mod_mask()
        if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK
                and not self.props.selection_mode
                and self.props.selection_mode_allowed):
            self.props.selection_mode = True

        (_, button) = event.get_button()
        if (button == Gdk.BUTTON_PRIMARY
                and not self.props.selection_mode):
            self.emit('song-activated', widget)

        if self.props.selection_mode:
            itr = widget.itr
            self._model[itr][6] = not self._model[itr][6]

        return True

    @log
    def _model_row_changed(self, model, path, itr):
        if (not self.props.selection_mode
                or not model[itr][5]):
            return

        song_widget = model[itr][5].song_widget
        selected = model[itr][6]
        if selected != song_widget.props.selected:
            song_widget.props.selected = selected

        return True
Ejemplo n.º 24
0
class SearchFilter(Gtk.HBox):
    """
    A base class used by common search filters
    """

    #: the label of this filter
    label = GObject.Property(type=str, flags=(GObject.ParamFlags.READWRITE))

    gsignal('changed')
    gsignal('removed')
    __gtype_name__ = 'SearchFilter'

    def __init__(self, label=''):
        super(SearchFilter, self).__init__()

        self.props.label = label
        self._label = label
        self._remove_button = None

    def _add_remove_button(self):
        self._remove_button = SearchFilterButton(icon='list-remove-symbolic')
        self._remove_button.set_relief(Gtk.ReliefStyle.NONE)
        self._remove_button.connect('clicked', self._on_remove_clicked)
        self._remove_button.show()
        self.pack_start(self._remove_button, False, False, 0)

    def _on_remove_clicked(self, button):
        self.emit('removed')

    def do_set_property(self, pspec, value):
        if pspec.name == 'label':
            self._label = value
        else:
            raise AssertionError(pspec.name)

    def do_get_property(self, child, property_id, pspec):
        if pspec.name == 'label':
            return self._label
        else:
            raise AssertionError(pspec.name)

    def set_label(self, label):
        self._label = label

    def get_state(self):
        """
        Implement this in a subclass
        """
        raise NotImplementedError

    def get_title_label(self):
        raise NotImplementedError

    def get_mode_combo(self):
        raise NotImplementedError

    def get_description(self):
        """Returns a description of the search filter.
        :returns: a string describing the search filter.
        """
        raise NotImplementedError

    def set_removable(self):
        if self._remove_button is None:
            self._add_remove_button()
Ejemplo n.º 25
0
class GSettingsBoolComboBox(GSettingsComboBox):

    __gtype_name__ = "GSettingsBoolComboBox"

    gsettings_column = GObject.Property(type=int, default=0)
    gsettings_value = GObject.Property(type=bool, default=False)
Ejemplo n.º 26
0
class CodeCommentViewActivatable(GObject.Object, Gedit.ViewActivatable):

    view = GObject.Property(type=Gedit.View)

    def __init__(self):
        self.popup_handler_id = 0
        GObject.Object.__init__(self)

    def do_activate(self):
        self.view.code_comment_view_activatable = self
        self.popup_handler_id = self.view.connect('populate-popup',
                                                  self.populate_popup)

    def do_deactivate(self):
        if self.popup_handler_id != 0:
            self.view.disconnect(self.popup_handler_id)
            self.popup_handler_id = 0
        delattr(self.view, "code_comment_view_activatable")

    def populate_popup(self, view, popup):
        if not isinstance(popup, Gtk.MenuShell):
            return

        item = Gtk.SeparatorMenuItem()
        item.show()
        popup.append(item)

        item = Gtk.MenuItem.new_with_mnemonic(_("Co_mment Code"))
        item.set_sensitive(self.doc_has_comment_tags())
        item.show()
        item.connect('activate', lambda i: self.do_comment(view.get_buffer()))
        popup.append(item)

        item = Gtk.MenuItem.new_with_mnemonic(_('U_ncomment Code'))
        item.set_sensitive(self.doc_has_comment_tags())
        item.show()
        item.connect('activate',
                     lambda i: self.do_comment(view.get_buffer(), True))
        popup.append(item)

    def doc_has_comment_tags(self):
        has_comment_tags = False
        doc = self.view.get_buffer()
        if doc:
            lang = doc.get_language()
            if lang is not None:
                has_comment_tags = self.get_comment_tags(lang) != (None, None)
        return has_comment_tags

    def get_block_comment_tags(self, lang):
        start_tag = lang.get_metadata('block-comment-start')
        end_tag = lang.get_metadata('block-comment-end')
        if start_tag and end_tag:
            return (start_tag, end_tag)
        return (None, None)

    def get_line_comment_tags(self, lang):
        start_tag = lang.get_metadata('line-comment-start')
        if start_tag:
            return (start_tag, None)
        return (None, None)

    def get_comment_tags(self, lang):
        if lang.get_id() in block_comment_languages:
            (s, e) = self.get_block_comment_tags(lang)
            if (s, e) == (None, None):
                (s, e) = self.get_line_comment_tags(lang)
        else:
            (s, e) = self.get_line_comment_tags(lang)
            if (s, e) == (None, None):
                (s, e) = self.get_block_comment_tags(lang)
        return (s, e)

    def forward_tag(self, iter, tag):
        iter.forward_chars(len(tag))

    def backward_tag(self, iter, tag):
        iter.backward_chars(len(tag))

    def get_tag_position_in_line(self, tag, head_iter, iter):
        found = False
        while (not found) and (not iter.ends_line()):
            s = iter.get_slice(head_iter)
            if s == tag:
                found = True
            else:
                head_iter.forward_char()
                iter.forward_char()
        return found

    def add_comment_characters(self, document, start_tag, end_tag, start, end):
        smark = document.create_mark("start", start, False)
        imark = document.create_mark("iter", start, False)
        emark = document.create_mark("end", end, False)
        number_lines = end.get_line() - start.get_line() + 1

        document.begin_user_action()

        for i in range(0, number_lines):
            iter = document.get_iter_at_mark(imark)
            if not iter.ends_line():
                document.insert(iter, start_tag)
                if end_tag is not None:
                    if i != number_lines - 1:
                        iter = document.get_iter_at_mark(imark)
                        iter.forward_to_line_end()
                        document.insert(iter, end_tag)
                    else:
                        iter = document.get_iter_at_mark(emark)
                        document.insert(iter, end_tag)
            iter = document.get_iter_at_mark(imark)
            iter.forward_line()
            document.delete_mark(imark)
            imark = document.create_mark("iter", iter, True)

        document.end_user_action()

        document.delete_mark(imark)
        new_start = document.get_iter_at_mark(smark)
        new_end = document.get_iter_at_mark(emark)
        if not new_start.ends_line():
            self.backward_tag(new_start, start_tag)
        document.select_range(new_start, new_end)
        document.delete_mark(smark)
        document.delete_mark(emark)

    def remove_comment_characters(self, document, start_tag, end_tag, start,
                                  end):
        smark = document.create_mark("start", start, False)
        emark = document.create_mark("end", end, False)
        number_lines = end.get_line() - start.get_line() + 1
        iter = start.copy()
        head_iter = iter.copy()
        self.forward_tag(head_iter, start_tag)

        document.begin_user_action()

        for i in range(0, number_lines):
            if self.get_tag_position_in_line(start_tag, head_iter, iter):
                dmark = document.create_mark("delete", iter, False)
                document.delete(iter, head_iter)
                if end_tag is not None:
                    iter = document.get_iter_at_mark(dmark)
                    head_iter = iter.copy()
                    self.forward_tag(head_iter, end_tag)
                    if self.get_tag_position_in_line(end_tag, head_iter, iter):
                        document.delete(iter, head_iter)
                document.delete_mark(dmark)
            iter = document.get_iter_at_mark(smark)
            iter.forward_line()
            document.delete_mark(smark)
            head_iter = iter.copy()
            self.forward_tag(head_iter, start_tag)
            smark = document.create_mark("iter", iter, True)

        document.end_user_action()

        document.delete_mark(smark)
        document.delete_mark(emark)

    def do_comment(self, document, unindent=False):
        sel = document.get_selection_bounds()
        currentPosMark = document.get_insert()
        deselect = False
        if sel != ():
            (start, end) = sel
            if start.ends_line():
                start.forward_line()
            elif not start.starts_line():
                start.set_line_offset(0)
            if end.starts_line():
                end.backward_char()
            elif not end.ends_line():
                end.forward_to_line_end()
        else:
            deselect = True
            start = document.get_iter_at_mark(currentPosMark)
            start.set_line_offset(0)
            end = start.copy()
            end.forward_to_line_end()

        lang = document.get_language()
        if lang is None:
            return

        (start_tag, end_tag) = self.get_comment_tags(lang)

        if not start_tag and not end_tag:
            return

        if unindent:  # Select the comment or the uncomment method
            new_code = self.remove_comment_characters(document, start_tag,
                                                      end_tag, start, end)
        else:
            new_code = self.add_comment_characters(document, start_tag,
                                                   end_tag, start, end)

        if deselect:
            oldPosIter = document.get_iter_at_mark(currentPosMark)
            document.select_range(oldPosIter, oldPosIter)
            document.place_cursor(oldPosIter)
Ejemplo n.º 27
0
class BaseView(Gtk.Stack):
    """Base Class for all view classes"""

    _now_playing_icon_name = 'media-playback-start-symbolic'
    _error_icon_name = 'dialog-error-symbolic'
    selection_mode = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<BaseView>'

    @log
    def __init__(self, name, title, window, view_type, use_sidebar=False,
                 sidebar=None):
        """Initialize
        :param name: The view name
        :param title: The view title
        :param GtkWidget window: The main window
        :param view_type: The Gtk view type
        :param use_sidebar: Whether to use sidebar
        :param sidebar: The sidebar object (Default: Gtk.Box)
        """

        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self.model = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,
            GObject.TYPE_OBJECT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,
            GObject.TYPE_STRING,
            GObject.TYPE_INT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT
        )
        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT,)
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self._star_handler = StarHandlerWidget(self, 9)
        self._window = window
        self._header_bar = window.toolbar
        self._selection_toolbar = window.selection_toolbar
        self._header_bar._select_button.connect(
           'toggled', self._on_header_bar_toggled)
        self._header_bar._cancel_button.connect(
            'clicked', self._on_cancel_button_clicked)

        self.name = name
        self.title = title

        self.add(self._grid)
        self.show_all()
        self._view.hide()

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.medium)

        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self._header_bar.connect('selection-mode-changed',
                                 self._on_selection_mode_changed)
        grilo.connect('changes-pending', self._on_changes_pending)

    @log
    def _on_changes_pending(self, data=None):
        pass

    @log
    def _setup_view(self, view_type):
        """Instantiate and set up the view object"""
        self._view = Gd.MainView(shadow_type=Gtk.ShadowType.NONE)
        self._view.set_view_type(view_type)

        self._view.click_handler = self._view.connect('item-activated',
                                                      self._on_item_activated)
        self._view.connect('selection-mode-request',
                           self._on_selection_mode_request)

        self._view.bind_property('selection-mode', self, 'selection_mode',
                                 GObject.BindingFlags.BIDIRECTIONAL)

        self._view.connect('view-selection-changed',
                           self._on_view_selection_changed)

        self._box.pack_start(self._view, True, True, 0)

    @log
    def _on_header_bar_toggled(self, button):
        self.selection_mode = button.get_active()

        if self.selection_mode:
            self._header_bar.set_selection_mode(True)
            self.player.actionbar.set_visible(False)
            select_toolbar = self._selection_toolbar
            select_toolbar.actionbar.set_visible(True)
            select_toolbar._add_to_playlist_button.set_sensitive(False)
            select_toolbar._remove_from_playlist_button.set_sensitive(False)
        else:
            self._header_bar.set_selection_mode(False)
            track_playing = self.player.currentTrack is not None
            self.player.actionbar.set_visible(track_playing)
            self._selection_toolbar.actionbar.set_visible(False)
            self.unselect_all()

    @log
    def _on_cancel_button_clicked(self, button):
        self._view.set_selection_mode(False)
        self._header_bar.set_selection_mode(False)

    @log
    def _on_grilo_ready(self, data=None):
        if (self._header_bar.get_stack().get_visible_child() == self
                and not self._init):
            self._populate()
        self._header_bar.get_stack().connect('notify::visible-child',
                                             self._on_headerbar_visible)

    @log
    def _on_headerbar_visible(self, widget, param):
        if (self == widget.get_visible_child()
                and not self._init):
            self._populate()

    @log
    def _on_view_selection_changed(self, widget):
        if not self.selection_mode:
            return
        items = self._view.get_selection()
        self.update_header_from_selection(len(items))

    @log
    def update_header_from_selection(self, n_items):
        """Updates header during item selection."""
        select_toolbar = self._selection_toolbar
        select_toolbar._add_to_playlist_button.set_sensitive(n_items > 0)
        select_toolbar._remove_from_playlist_button.set_sensitive(n_items > 0)
        if n_items > 0:
            self._header_bar._selection_menu_label.set_text(
                ngettext("Selected {} item",
                         "Selected {} items",
                         n_items).format(n_items))
        else:
            self._header_bar._selection_menu_label.set_text(
                _("Click on items to select them"))

    @log
    def _populate(self, data=None):
        self._init = True
        self.populate()

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        pass

    @log
    def populate(self):
        pass

    @log
    def _add_item(self, source, param, item, remaining=0, data=None):
        if not item:
            if remaining == 0:
                self._view.set_model(self.model)
                self._window.pop_loading_notification()
                self._view.show()
            return
        self._offset += 1
        artist = utils.get_artist_name(item)
        title = utils.get_media_title(item)

        itr = self.model.append(None)
        loading_icon = Gdk.pixbuf_get_from_surface(
            self._loadin_icon_surface, 0, 0,
            self._loading_icon_surface.get_width(),
            self._loading_icon_surface.get_height())

        self.model[itr][0, 1, 2, 3, 4, 5, 7, 9] = [
            str(item.get_id()),
            '',
            title,
            artist,
            loading_icon,
            item,
            0,
            False
        ]

    @log
    def _on_lookup_ready(self, surface, itr):
        if surface:
            pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
                                                 surface.get_width(),
                                                 surface.get_height())
            self.model[itr][4] = pixbuf

    @log
    def _add_list_renderers(self):
        pass

    @log
    def _on_item_activated(self, widget, id, path):
        pass

    @log
    def _on_selection_mode_request(self, *args):
        self._header_bar._select_button.clicked()

    @log
    def get_selected_songs(self, callback):
        callback([])

    @log
    def _set_selection(self, value, parent=None):
        count = 0
        itr = self.model.iter_children(parent)
        while itr != None:
            if self.model.iter_has_child(itr):
                count += self._set_selection(value, itr)
            if self.model[itr][5] != None:
                self.model[itr][6] = value
                count += 1
            itr = self.model.iter_next(itr)
        return count

    @log
    def select_all(self):
        """Select all the available songs."""
        count = self._set_selection(True)

        if count > 0:
            select_toolbar = self._selection_toolbar
            select_toolbar._add_to_playlist_button.set_sensitive(True)
            select_toolbar._remove_from_playlist_button.set_sensitive(True)

        self.update_header_from_selection(count)
        self._view.queue_draw()

    @log
    def unselect_all(self):
        """Unselects all the selected songs."""
        self._set_selection(False)
        select_toolbar = self._selection_toolbar
        select_toolbar._add_to_playlist_button.set_sensitive(False)
        select_toolbar._remove_from_playlist_button.set_sensitive(False)
        self._header_bar._selection_menu_label.set_text(
             _("Click on items to select them"))
        self.queue_draw()
Ejemplo n.º 28
0
class PreferencesDialog(Gtk.Dialog):
    __gtype_name__ = 'PreferencesDialog'

    local_stack = GtkTemplate.Child()
    remote_stack = GtkTemplate.Child()
    remote_page_stack = GtkTemplate.Child()
    remote_page_box = GtkTemplate.Child()
    disconnected_page = GtkTemplate.Child()
    client = GObject.Property(type=Client, flags=GObject.ParamFlags.CONSTRUCT_ONLY|GObject.ParamFlags.READWRITE)

    def __init__(self, **kwargs):
        super().__init__(use_header_bar=1, **kwargs)
        self.init_template()

        # ---------- Local Settings --------------
        self.settings = Gio.Settings.new('se.tingping.Trg')

        local_pages = (
            Page('connection', _('Connection'), (
                Row(_('Hostname'), Gtk.Entry.new(), 'text', 'hostname'),
                Row(_('Port'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1), 'value', 'port'),
                Row(_('Username'), Gtk.Entry.new(), 'text', 'username'),
                Row(_('Password'), Gtk.Entry(visibility=False, input_purpose=Gtk.InputPurpose.PASSWORD), 'text',
                    'password'),
                Row(_('Connect over HTTPS'), Gtk.Switch.new(), 'active', 'tls'),
            )),
            Page('service', _('Service'), [
                Row(_('Automatically Load Torrent Files'), Gtk.Switch.new(), 'active',
                    'watch-downloads-directory'),
                Row(_('Show Notifications on Completion'), Gtk.Switch.new(), 'active',
                    'notify-on-finish'),
            ]),
        )
        if not is_flatpak():
            self._autostart_switch = AutoStartSwitch()
            as_row = Row(_('Autostart service on login'), self._autostart_switch, '', '')
            local_pages[1].rows.append(as_row)

        if _get_has_statusnotifier():
            row = Row(_('Show status icon'), Gtk.Switch.new(), 'active', 'show-status-icon')
            local_pages[1].rows.append(row)

        bind_flags = Gio.SettingsBindFlags.DEFAULT|Gio.SettingsBindFlags.NO_SENSITIVITY
        self._create_settings_pane(local_pages, self.local_stack,
                                   lambda wid, prop, setting: self.settings.bind(setting, wid, prop, bind_flags))

        # ------------- Remote Page ---------------
        self.remote_settings = RemoteSettings(self.client)
        encryption_combo = Gtk.ComboBoxText.new()
        for val in (('required', _('Required')), ('preferred', _('Preferred')), ('tolerated', _('Tolerated'))):
            encryption_combo.append(*val)

        seed_button = Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, .1)
        seed_button.props.digits = 2

        def make_link(url, text):
            return '<a href="{}">{}</a>'.format(url, GLib.markup_escape_text(text))

        remote_pages = (
            Page('general', _('General'), (
                Row(_('Download Directory'), Gtk.Entry.new(), 'text', 'download-dir'),
                ToggledRow(_('Incomplete Directory'), Gtk.Entry.new(), 'text',
                           'incomplete-dir', 'incomplete-dir-enabled'),
                Row(_('Append ".part" to Incomplete'), Gtk.Switch.new(), 'active', 'rename-partial-files'),
            )),
            Page('connections', _('Connection'), (
                Row(_('Peer Port'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1), 'value', 'peer-port'),
                Row(_('Encryption'), encryption_combo, 'active-id', 'encryption'),
                ToggledRow(_('Blocklist URL'), Gtk.Entry.new(), 'text',
                           'blocklist-url', 'blocklist-enabled'),
                Row(_('Randomize Port on Start'), Gtk.Switch.new(), 'active', 'peer-port-random-on-start'),
                Row(make_link('https://en.wikipedia.org/wiki/Distributed_hash_table', _('Distributed Hash Table (DHT)')),
                    Gtk.Switch.new(), 'active', 'dht-enabled'),
                Row(make_link('https://en.wikipedia.org/wiki/Peer_exchange', _('Peer Exchange (PEX)')),
                    Gtk.Switch.new(), 'active', 'pex-enabled'),
                Row(make_link('https://en.wikipedia.org/wiki/Local_Peer_Discovery', _('Local Peer Discovery')),
                    Gtk.Switch.new(), 'active', 'lpd-enabled'),
                Row(make_link('https://en.wikipedia.org/wiki/Micro_Transport_Protocol', _('Micro Transport Protocol (µTP)')),
                    Gtk.Switch.new(), 'active', 'utp-enabled'),
                Row(' '.join((GLib.markup_escape_text(_('Port fowarding')),
                             make_link('https://en.wikipedia.org/wiki/NAT_Port_Mapping_Protocol', '(NAT-PMP)'),
                             make_link('https://en.wikipedia.org/wiki/Universal_Plug_and_Play', '(UPnP)'))),
                    Gtk.Switch.new(), 'active', 'port-forwarding-enabled'),

            )),
            # TODO: Add headers
            # TODO: Add toggles for these
            Page('limits', _('Limits'), (
                ToggledRow(_('Download Limit (KB/s)'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT32, 10),
                           'value', 'speed-limit-down', 'speed-limit-down-enabled'),
                ToggledRow(_('Upload Limit (KB/s)'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT32, 10),
                           'value', 'speed-limit-up', 'speed-limit-up-enabled'),
                ToggledRow(_('Seed Ratio Limit'), seed_button,
                           'value', 'seedRatioLimit', 'seedRatioLimited'),
                ToggledRow(_('Download Queue Size'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1),
                           'value', 'download-queue-size', 'download-queue-enabled'),
                ToggledRow(_('Seed Queue Size'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1),
                           'value', 'seed-queue-size', 'seed-queue-enabled'),
                ToggledRow(_('Idle Seed Queue Size'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1),
                           'value', 'idle-seeding-limit', 'idle-seeding-limit-enabled'),
                Row(_('Global Peer Limit'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1),
                    'value', 'peer-limit-global',),
                Row(_('Per-Torrent Peer Limit'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT16, 1),
                    'value', 'peer-limit-per-torrent',),
            )),
            Page('alt-limits', _('Alternate Limits'), (
                Row(_('Alternative Limits Active'), Gtk.Switch.new(), 'active', 'alt-speed-enabled'),
                Row(_('Alternate Down Limit (KB/s)'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT32, 10),
                    'value', 'alt-speed-down'),
                Row(_('Alternate Up Limit (KB/s)'), Gtk.SpinButton.new_with_range(0, GLib.MAXUINT32, 10),
                    'value', 'alt-speed-up'),
                # TODO: Alt-time limits
            )),
        )

        self._create_settings_pane(remote_pages, self.remote_stack, self.remote_settings.bind_setting)
        if not self.client.props.connected:  # TODO: Handle connection changes
            self.remote_page_stack.props.visible_child = self.disconnected_page
        else:
            self.remote_settings.refresh(self._on_remote_settings_refresh)

    def _on_remote_settings_refresh(self):
        self.remote_page_stack.props.visible_child = self.remote_page_box

    def _create_settings_pane(self, pages, stack, bind_func):
        for page in pages:
            id_, title, rows = page
            grid = Gtk.Grid(visible=True, margin=18,
                            column_spacing=12, row_spacing=6)
            for i, row in enumerate(rows):
                label = Gtk.Label(label=row.title, halign=Gtk.Align.END, visible=True)
                if '<a href' in row.title:
                    label.props.use_markup = True
                label.get_style_context().add_class('dim-label')
                row.widget.props.hexpand = True
                row.widget.show()
                grid.attach(label, 0, i, 1, 1)

                if isinstance(row, ToggledRow):
                    widget = Gtk.Box(visible=True, spacing=6)
                    toggle = Gtk.Switch(visible=True, valign=Gtk.Align.CENTER)
                    bind_func(toggle, 'active', row.toggle_setting)
                    toggle.bind_property('active', row.widget, 'sensitive',
                                         GObject.BindingFlags.SYNC_CREATE)
                    widget.add(toggle)
                    widget.add(row.widget)
                else:
                    widget = row.widget
                if isinstance(widget, Gtk.Switch):
                    widget.props.halign = Gtk.Align.START
                grid.attach(widget, 1, i, 1, 1)

                if row.bind_property and row.setting:
                    bind_func(row.widget, row.bind_property, row.setting)
            stack.add_titled(grid, id_, title)

    def do_show(self):
        Gtk.Dialog.do_show(self)
        self.settings.delay()

    def do_response(self, response_id):
        if response_id == Gtk.ResponseType.APPLY:
            self.settings.apply()
            self.remote_settings.apply()
            if not is_flatpak():
                self._autostart_switch.apply()
        else:
            self.settings.revert()

        if response_id != Gtk.ResponseType.DELETE_EVENT:
            self.destroy()
Ejemplo n.º 29
0
class ArtistAlbumsWidget(Gtk.Box):
    """Widget containing all albums by an artist

    A vertical list of ArtistAlbumWidget, containing all the albums
    by one artist. Contains the model for all the song widgets of
    the album(s).
    """

    __gtype_name__ = 'ArtistAlbumsWidget'

    _artist_label = Gtk.Template.Child()

    selected_items_count = GObject.Property(type=int, default=0, minimum=0)
    selection_mode = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<ArtistAlbumsWidget>'

    @log
    def __init__(self,
                 artist,
                 albums,
                 player,
                 window,
                 selection_mode_allowed=False):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)
        self._player = player
        self._artist = artist
        self._window = window
        self._selection_mode_allowed = selection_mode_allowed

        self._artist_label.props.label = self._artist

        self._widgets = []

        self._create_model()

        self._model.connect('row-changed', self._model_row_changed)

        hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._album_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
                                  spacing=48)
        hbox.pack_start(self._album_box, False, False, 16)

        self._scrolled_window = Gtk.ScrolledWindow()
        self._scrolled_window.set_policy(Gtk.PolicyType.NEVER,
                                         Gtk.PolicyType.AUTOMATIC)
        self._scrolled_window.add(hbox)
        self.pack_start(self._scrolled_window, True, True, 0)

        self._cover_size_group = Gtk.SizeGroup.new(
            Gtk.SizeGroupMode.HORIZONTAL)
        self._songs_grid_size_group = Gtk.SizeGroup.new(
            Gtk.SizeGroupMode.HORIZONTAL)

        self._window.notifications_popup.push_loading()

        self._albums_to_load = len(albums)
        for album in albums:
            self._add_album(album)

        self._player.connect('song-changed', self._update_model)

    @log
    def _create_model(self):
        """Create the ListStore model for this widget."""
        self._model = Gtk.ListStore(
            GObject.TYPE_STRING,  # title
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,  # placeholder
            GObject.TYPE_OBJECT,  # song object
            GObject.TYPE_BOOLEAN,  # item selected
            GObject.TYPE_STRING,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,  # icon shown
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT)

    @log
    def _on_album_displayed(self, data=None):
        self._albums_to_load -= 1
        if self._albums_to_load == 0:
            self._window.notifications_popup.pop_loading()
            self.show_all()

    @log
    def _add_album(self, album):
        widget = ArtistAlbumWidget(album, self._player, self._model,
                                   self._selection_mode_allowed,
                                   self._songs_grid_size_group,
                                   self._cover_size_group)

        self.bind_property(
            'selection-mode', widget, 'selection-mode',
            GObject.BindingFlags.BIDIRECTIONAL
            | GObject.BindingFlags.SYNC_CREATE)

        self._album_box.pack_start(widget, False, False, 0)
        self._widgets.append(widget)

        widget.connect('songs-loaded', self._on_album_displayed)

    @log
    def _update_model(self, player):
        """Updates model when the song changes

        :param Player player: The main player object
        """
        if not player.playing_playlist(PlayerPlaylist.Type.ARTIST,
                                       self._artist):
            self._clean_model()
            return False

        current_song = player.props.current_song
        song_passed = False
        itr = self._model.get_iter_first()

        while itr:
            song = self._model[itr][5]
            song_widget = song.song_widget

            if (song.get_id() == current_song.get_id()):
                song_widget.props.state = SongWidget.State.PLAYING
                song_passed = True
            elif (song_passed):
                # Counter intuitive, but this is due to call order.
                song_widget.props.state = SongWidget.State.UNPLAYED
            else:
                song_widget.props.state = SongWidget.State.PLAYED

            itr = self._model.iter_next(itr)

        return False

    @log
    def _clean_model(self):
        itr = self._model.get_iter_first()

        while itr:
            song = self._model[itr][5]
            song_widget = song.song_widget
            song_widget.props.state = SongWidget.State.UNPLAYED

            itr = self._model.iter_next(itr)

        return False

    @log
    def _model_row_changed(self, model, path, itr):
        if not self.props.selection_mode:
            return

        selected_items = 0
        for row in model:
            if row[6]:
                selected_items += 1

        self.props.selected_items_count = selected_items

    @log
    def select_all(self):
        """Select all items"""
        for widget in self._widgets:
            widget.select_all()

    @log
    def select_none(self):
        """Deselect all items"""
        for widget in self._widgets:
            widget.select_none()

    @GObject.Property(type=str, flags=GObject.ParamFlags.READABLE)
    def artist(self):
        """Artist name"""
        return self._artist

    @log
    def get_selected_songs(self):
        """Return a list of selected songs.

        :returns: selected songs
        :rtype: list
        """
        songs = []
        for widget in self._widgets:
            songs += widget.get_selected_songs()
        return songs
Ejemplo n.º 30
0
class AccountConfig(Gtk.Overlay):
    __gtype_name__ = 'AccountConfig'
    # Signals
    __gsignals__ = {
        'changed': (
            GObject.SignalFlags.RUN_LAST,
            None, (bool,)
        ),
    }
    # Properties
    is_edit = GObject.Property(type=bool, default=False)
    # Widgets
    main_container: Gtk.Box = Gtk.Template.Child()

    proivder_image: ProviderImage

    provider_combobox = Gtk.Template.Child()
    providers_store = Gtk.Template.Child()
    provider_entry: Gtk.Entry = Gtk.Template.Child()

    account_name_entry: Gtk.Entry = Gtk.Template.Child()
    provider_website_entry: Gtk.Entry = Gtk.Template.Child()
    token_entry: Gtk.Entry = Gtk.Template.Child()

    def __init__(self, **kwargs):
        super(AccountConfig, self).__init__()
        self.init_template('AccountConfig')

        self.props.is_edit = kwargs.get("edit", False)
        self._account = kwargs.get("account", None)
        self._notification = Notification()
        self.__init_widgets()

    @property
    def account(self):
        """
            Return an instance of Account for the new account.
        """
        provider_name = self.provider_entry.get_text()
        provider = Provider.get_by_name(provider_name)

        # Create a new provider if we don't find one
        if not provider:
            provider_image = self.provider_image.image
            provider_website = self.provider_website_entry.get_text()
            provider = Provider.create(provider_name, provider_website, None,
                                       provider_image)
        # Update the provider image if it changed
        elif provider and self.provider_image.image != provider.image:
            provider.update(image=self.provider_image.image)

        account = {
            "username": self.account_name_entry.get_text(),
            "provider": provider
        }
        if not self.props.is_edit:
            # remove spaces
            token = self.token_entry.get_text()
            account["token"] = "".join(token.split())
        return account

    def __init_widgets(self):
        self.add_overlay(self._notification)
        if self._account is not None:
            self.provider_image = ProviderImage(self._account.provider,
                                                96)
            self.token_entry.props.secondary_icon_activatable = self._account.provider.doc_url is not None
        else:
            self.token_entry.props.secondary_icon_activatable = False
            self.provider_image = ProviderImage(None, 96)

        self.main_container.pack_start(self.provider_image, False, False, 0)
        self.main_container.reorder_child(self.provider_image, 0)
        self.provider_image.set_halign(Gtk.Align.CENTER)

        # Set up auto completion
        if self._account and self._account.provider:
            self.provider_entry.set_text(self._account.provider.name)

        if self._account and self._account.username:
            self.account_name_entry.set_text(self._account.username)

        if self.props.is_edit:
            self.token_entry.hide()
            self.token_entry.set_no_show_all(True)
        else:
            self.token_entry.connect("icon-press", self.__on_open_doc_url)

        self._fill_data()

    def __on_open_doc_url(self, *args):
        provider_name = self.provider_entry.get_text()
        provider = Provider.get_by_name(provider_name)
        if provider and provider.doc_url:
            Gio.app_info_launch_default_for_uri(provider.doc_url)
        else:
            self.token_entry.props.secondary_icon_activatable = False

    @Gtk.Template.Callback('provider_changed')
    def _on_provider_changed(self, combo):
        tree_iter = combo.get_active_iter()
        if tree_iter is not None:
            model = combo.get_model()
            provider_id = model[tree_iter][0]
            provider = Provider.get_by_id(provider_id)
        else:
            provider_name = self.provider_entry.get_text()
            provider = Provider.get_by_name(provider_name)
        # if we find a provider already saved on the database
        if provider:
            self.token_entry.props.secondary_icon_activatable = provider.doc_url is not None
            self.provider_image.emit("provider-changed", provider)
            self.provider_website_entry.hide()
            self.provider_website_entry.set_no_show_all(True)
        else:
            self.provider_website_entry.show()
            self.provider_website_entry.set_no_show_all(False)
            self.provider_image.set_state(ProviderImageState.NOT_FOUND)

    def _fill_data(self):
        providers = Provider.all()
        for provider in providers:
            self.providers_store.append([provider.provider_id, provider.name])

    @Gtk.Template.Callback('account_edited')
    def _validate(self, *_):
        """Validate the username and the token."""
        provider = self.provider_entry.get_text()
        username = self.account_name_entry.get_text()
        token = "".join(self.token_entry.get_text().split())

        if not username:
            self.account_name_entry.get_style_context().add_class("error")
            valid_name = False
        else:
            self.account_name_entry.get_style_context().remove_class("error")
            valid_name = True

        if not provider:
            self.provider_combobox.get_style_context().add_class("error")
            valid_provider = False
        else:
            self.provider_combobox.get_style_context().remove_class("error")
            valid_provider = True

        if (not token or not OTP.is_valid(token)) and not self.props.is_edit:
            self.token_entry.get_style_context().add_class("error")
            valid_token = False
        else:
            self.token_entry.get_style_context().remove_class("error")
            valid_token = True

        self.emit("changed", all([valid_name, valid_provider, valid_token]))

    @Gtk.Template.Callback('on_provider_website_changed')
    def on_provider_website_changed(self, entry, event):
        '''Update the website favicon once the URL is updated'''
        if entry.get_visible():
            website = entry.get_text().strip()
            self.provider_image.fetch_favicon_from_url(website)

    def scan_qr(self, *args):
        '''Scans a QRCode and fills the entries with the correct data.'''
        try:
            filename = GNOMEScreenshot.area()
            assert filename
            account = QRReader.from_file(filename)
            assert account is dict
            self.token_entry.set_text(account.get('token',
                                                  self.token_entry.get_text()))
            self.provider_entry.set_text(account.get('provider',
                                                     self.provider_entry.get_text()))
            self.account_name_entry.set_text(account.get('username',
                                                         self.account_name_entry.get_text()))
        except AssertionError:
            self._notification.send(_("Invalid QR code"),
                                    timeout=3)