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
class GSettingsStringComboBox(GSettingsComboBox): __gtype_name__ = "GSettingsStringComboBox" gsettings_column = GObject.Property(type=int, default=0) gsettings_value = GObject.Property(type=str, default="")
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")
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
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))
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))
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
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
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
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)
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)
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)))
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)
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)
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)
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)
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))
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
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)
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
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)
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}')
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
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()
class GSettingsBoolComboBox(GSettingsComboBox): __gtype_name__ = "GSettingsBoolComboBox" gsettings_column = GObject.Property(type=int, default=0) gsettings_value = GObject.Property(type=bool, default=False)
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)
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()
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()
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
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)