Beispiel #1
0
    def set_column_headers(self, headers):
        if len(headers) == 0:
            return

        self.handler_block(self.__csig)

        old_sort = self.get_sort_orders()
        for column in self.get_columns():
            self.remove_column(column)

        if self._first_column:
            column = self._first_column()
            self.append_column(column)

        cws = config.getstringlist("memory", "column_widths")
        column_widths = {}
        for i in range(0, len(cws), 2):
            column_widths[cws[i]] = int(cws[i + 1])

        ce = config.getstringlist("memory", "column_expands")
        column_expands = {}
        for i in range(0, len(ce), 2):
            column_expands[ce[i]] = int(ce[i + 1])

        for t in headers:
            column = create_songlist_column(t)
            if column.get_resizable():
                if t in column_widths:
                    column.set_fixed_width(column_widths[t])
                if t in column_expands:
                    column.set_expand(column_expands[t])
                else:
                    column.set_expand(True)

            def column_clicked(column, *args):
                # if ctrl is held during the sort click, append a sort key
                # or change order if already sorted
                ctrl_held = False
                event = Gtk.get_current_event()
                if event:
                    ok, state = event.get_state()
                    if ok and state & Gdk.ModifierType.CONTROL_MASK:
                        ctrl_held = True

                self.toggle_column_sort(column, replace=not ctrl_held)

            column.connect('clicked', column_clicked)
            column.connect('button-press-event', self.__showmenu)
            column.connect('popup-menu', self.__showmenu)
            column.connect('notify::width', self.__column_width_changed)
            column.set_reorderable(True)
            self.append_column(column)

        self.columns_autosize()
        self.set_sort_orders(old_sort)

        self.handler_unblock(self.__csig)
Beispiel #2
0
    def set_column_headers(self, headers):
        if len(headers) == 0:
            return

        self.handler_block(self.__csig)

        old_sort = self.get_sort_orders()
        for column in self.get_columns():
            self.remove_column(column)

        if self._first_column:
            column = self._first_column()
            self.append_column(column)

        cws = config.getstringlist("memory", "column_widths")
        column_widths = {}
        for i in range(0, len(cws), 2):
            column_widths[cws[i]] = int(cws[i + 1])

        ce = config.getstringlist("memory", "column_expands")
        column_expands = {}
        for i in range(0, len(ce), 2):
            column_expands[ce[i]] = int(ce[i + 1])

        for t in headers:
            column = create_songlist_column(t)
            if column.get_resizable():
                if t in column_widths:
                    column.set_fixed_width(column_widths[t])
                if t in column_expands:
                    column.set_expand(column_expands[t])
                else:
                    column.set_expand(True)

            def column_clicked(column, *args):
                # if ctrl is held during the sort click, append a sort key
                # or change order if already sorted
                ctrl_held = False
                event = Gtk.get_current_event()
                if event:
                    ok, state = event.get_state()
                    if ok and state & qltk.get_primary_accel_mod():
                        ctrl_held = True

                self.toggle_column_sort(column, replace=not ctrl_held)

            column.connect('clicked', column_clicked)
            column.connect('button-press-event', self.__showmenu)
            column.connect('popup-menu', self.__showmenu)
            column.connect('notify::width', self.__column_width_changed)
            column.set_reorderable(True)
            self.append_column(column)

        self.set_sort_orders(old_sort)
        self.columns_autosize()

        self.handler_unblock(self.__csig)
    def set_column_headers(self, headers):
        if len(headers) == 0:
            return

        self.handler_block(self.__csig)

        old_sort = self.is_sorted() and self.get_sort_by()
        for column in self.get_columns():
            self.remove_column(column)

        if self._first_column:
            column = self._first_column()
            self.append_column(column)

        cws = config.getstringlist("memory", "column_widths")
        column_widths = {}
        for i in range(0, len(cws), 2):
            column_widths[cws[i]] = int(cws[i + 1])

        ce = config.getstringlist("memory", "column_expands")
        column_expands = {}
        for i in range(0, len(ce), 2):
            column_expands[ce[i]] = int(ce[i + 1])

        for t in headers:
            column = create_songlist_column(t)
            if column.get_resizable():
                if t in column_widths:
                    column.set_fixed_width(column_widths[t])
                if t in column_expands:
                    column.set_expand(column_expands[t])
                else:
                    column.set_expand(True)

            column.connect('clicked', self.set_sort_by)
            column.connect('button-press-event', self.__showmenu)
            column.connect('popup-menu', self.__showmenu)
            column.connect('notify::width', self.__column_width_changed)
            column.set_reorderable(True)
            self.append_column(column)

        self.columns_autosize()
        if old_sort:
            header, order = old_sort
            self.set_sort_by(None, header, order, False)

        self.handler_unblock(self.__csig)
Beispiel #4
0
def get_columns():
    """Gets the list of songlist column headings"""

    columns = config.getstringlist("settings", "columns",
                                   const.DEFAULT_COLUMNS)
    if "~current" in columns:
        columns.remove("~current")
    return columns
Beispiel #5
0
def get_columns():
    """Gets the list of songlist column headings"""

    columns = config.getstringlist("settings", "columns",
                                   const.DEFAULT_COLUMNS)
    if "~current" in columns:
        columns.remove("~current")
    return columns
Beispiel #6
0
    def _update(self, songs=None):
        if songs is None:
            songs = self._group_info.songs
        else:
            self._group_info = AudioFileGroup(songs)
        info = self._group_info

        keys = list(info.keys())
        default_tags = get_default_tags()
        keys = set(keys + default_tags)

        def custom_sort(key):
            try:
                prio = default_tags.index(key)
            except ValueError:
                prio = len(default_tags)
            return (prio, tagsortkey(key))

        if not config.getboolean("editing", "alltags"):
            keys = filter(lambda k: k not in MACHINE_TAGS, keys)

        if not config.getboolean("editing", "show_multi_line_tags"):
            tags = config.getstringlist("editing", "multi_line_tags")
            keys = filter(lambda k: k not in tags, keys)

        if not songs:
            keys = []

        with self._view.without_model() as model:
            model.clear()

            for tag in sorted(keys, key=custom_sort):
                canedit = info.can_change(tag)

                # default tags
                if tag not in info:
                    entry = ListEntry(tag, Comment(u""))
                    entry.canedit = canedit
                    model.append(row=[entry])
                    continue

                for value in info[tag]:
                    entry = ListEntry(tag, value)
                    entry.origvalue = value
                    entry.edited = False
                    entry.canedit = canedit
                    entry.deleted = False
                    entry.renamed = False
                    entry.origtag = ""
                    model.append(row=[entry])

        self._buttonbox.set_sensitive(bool(info.can_change()))
        self._revert.set_sensitive(False)
        self._remove.set_sensitive(False)
        self._save.set_sensitive(False)
        self._add.set_sensitive(bool(songs))
        self._parent.set_pending(None)
Beispiel #7
0
    def test_basic(self):
        self.assertTrue(config.get("memory", "pane_widths", None) is None)

        p = self.Kind("memory", "pane_widths")
        sws = [Gtk.ScrolledWindow() for _ in range(3)]
        p.set_widgets(sws)

        paneds = p._get_paneds()
        paneds[0].set_relative(0.4)
        paneds[1].set_relative(0.6)
        p.save_widths()

        widths = config.getstringlist("memory", "pane_widths")
        self.assertAlmostEqual(float(widths[0]), 0.4)
        self.assertAlmostEqual(float(widths[1]), 0.6)

        config.remove_option("memory", "pane_widths")
Beispiel #8
0
    def _restore_widths(self):
        """Restore pane widths from the config."""

        widths = config.getstringlist(self.section, self.option, [])
        paneds = self._get_paneds()

        if not widths:
            # If no widths are saved, save the current widths
            self.__changed()
        else:
            # Restore as many widths as we have saved
            # (and convert them from str to float)
            for i, width in enumerate(map(float, widths)):
                if i >= len(paneds):
                    break
                paneds[i].set_relative(width)
            self.__changed()
Beispiel #9
0
    def _restore_widths(self):
        """Restore pane widths from the config."""

        widths = config.getstringlist(self.section, self.option, [])
        paneds = self._get_paneds()

        if not widths:
            # If no widths are saved, save the current widths
            self.__changed()
        else:
            # Restore as many widths as we have saved
            # (and convert them from str to float)
            for i, width in enumerate(map(float, widths)):
                if i >= len(paneds):
                    break
                paneds[i].set_relative(width)
            self.__changed()
Beispiel #10
0
def get_columns():
    """Gets the list of songlist column headings"""

    if config.has_option("settings", "columns"):
        return config.getstringlist("settings", "columns",
                                    const.DEFAULT_COLUMNS)
    else:
        # migrate old settings
        try:
            columns = config.get("settings", "headers").split()
        except config.Error:
            return const.DEFAULT_COLUMNS
        else:
            config.remove_option("settings", "headers")
            set_columns(columns)
            config.setstringlist("settings", "columns", columns)
            return columns
Beispiel #11
0
def get_columns():
    """Gets the list of songlist column headings"""

    if config.has_option("settings", "columns"):
        return config.getstringlist(
            "settings", "columns", const.DEFAULT_COLUMNS)
    else:
        # migrate old settings
        try:
            columns = config.get("settings", "headers").split()
        except config.Error:
            return const.DEFAULT_COLUMNS
        else:
            config.remove_option("settings", "headers")
            set_columns(columns)
            config.setstringlist("settings", "columns", columns)
            return columns
Beispiel #12
0
    def test_basic(self):
        self.assertTrue(config.get("memory", "pane_widths", None) is None)

        p = self.Kind("memory", "pane_widths")
        sws = [Gtk.ScrolledWindow() for _ in range(3)]
        p.set_widgets(sws)

        paneds = p._get_paneds()
        paneds[0].set_relative(0.4)
        paneds[1].set_relative(0.6)
        p.save_widths()

        widths = config.getstringlist("memory", "pane_widths")
        self.assertAlmostEqual(float(widths[0]), 0.4)
        self.assertAlmostEqual(float(widths[1]), 0.6)

        config.remove_option("memory", "pane_widths")
Beispiel #13
0
 def config_get_stringlist(cls, name, default=False):
     """Gets a config string list for this plugin"""
     return config.getstringlist(PM.CONFIG_SECTION, cls._config_key(name),
                                 default)
Beispiel #14
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super(QuodLibetWindow, self).__init__(dialog=False)
        self.last_dir = get_home_dir()

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(550, 450)

        main_box = Gtk.VBox()
        self.add(main_box)

        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, True)

        keyval, mod = Gtk.accelerator_parse("<control><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # dbus app menu
        # Unity puts the app menu next to our menu bar. Since it only contains
        # menu items also available in the menu bar itself, don't add it.
        if not util.is_unity():
            AppMenu(self, ui.get_action_groups()[0])

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.show_all()
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after('drag-data-received',
                                    self.__songlist_drag_data_recv)
        self.song_scroller = SongListScroller(
            ui.get_widget("/Menu/View/SongList"))
        self.song_scroller.add(self.songlist)
        self.qexpander = QueueExpander(ui.get_widget("/Menu/View/Queue"),
                                       library, player)
        self.playlist = PlaylistMux(player, self.qexpander.model,
                                    self.songlist.model)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        main_box.pack_start(self.__browserbox, True, True, 0)

        statusbox = StatusBarBox(self.songlist.model, player)
        self.order = statusbox.order
        self.repeat = statusbox.repeat
        self.statusbar = statusbox.statusbar

        main_box.pack_start(Align(statusbox, border=3, top=-3, right=3), False,
                            True, 0)

        self.songpane = ConfigRVPaned("memory", "queue_position", 0.75)
        self.songpane.pack1(self.song_scroller, resize=True, shrink=False)
        self.songpane.pack2(self.qexpander, resize=True, shrink=False)
        self.__handle_position = self.songpane.get_property("position")

        def songpane_button_press_cb(pane, event):
            """If we start to drag the pane handle while the
            queue expander is unexpanded, expand it and move the handle
            to the bottom, so we can 'drag' the queue out
            """

            if event.window != pane.get_handle_window():
                return False

            if not self.qexpander.get_expanded():
                self.qexpander.set_expanded(True)
                pane.set_relative(1.0)
            return False

        self.songpane.connect("button-press-event", songpane_button_press_cb)

        self.song_scroller.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::expanded', self.__expand_or)
        self.qexpander.connect('draw', self.__qex_size_allocate)
        self.songpane.connect('notify', self.__moved_pane_handle)

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self.select_browser(self, config.get("memory", "browser"), library,
                                player, restore_browser)
        except:
            config.set("memory", "browser", browsers.name(0))
            config.save()
            raise

        self.showhide_playlist(ui.get_widget("/Menu/View/SongList"))
        self.showhide_playqueue(ui.get_widget("/Menu/View/Queue"))

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_time)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Beispiel #15
0
    def lyric_filename(self):
        """Returns the validated, or default, lyrics filename for this
        file. User defined '[memory] lyric_rootpaths' and
        '[memory] lyric_filenames' matches take precedence"""

        from quodlibet.pattern \
            import ArbitraryExtensionFileFromPattern as expand_patterns

        rx_params = re.compile(r'[^\\]<[^' + re.escape(os.sep) + r']*[^\\]>')

        def expand_pathfile(rpf):
            """Return the expanded RootPathFile"""
            expanded = []
            root = expanduser(rpf.root)
            pathfile = expanduser(rpf.pathfile)
            if rx_params.search(pathfile):
                root = expand_patterns(root).format(self)
                pathfile = expand_patterns(pathfile).format(self)
            rpf = RootPathFile(root, pathfile)
            expanded.append(rpf)
            if not os.path.exists(pathfile) and is_windows():
                # prioritise a special character encoded version
                #
                # most 'alien' chars are supported for 'nix fs paths, and we
                # only pass the proposed path through 'escape_filename' (which
                # apparently doesn't respect case) if we don't care about case!
                #
                # FIX: assumes 'nix build used on a case-sensitive fs, nt case
                # insensitive. clearly this is not biting anyone though (yet!)
                pathfile = os.path.sep.join([rpf.root, rpf.end_escaped])
                rpf = RootPathFile(rpf.root, pathfile)
                expanded.insert(len(expanded) - 1, rpf)
            return expanded

        def sanitise(sep, parts):
            """Return a santisied version of a path's parts"""
            return sep.join(
                part.replace(os.path.sep, u'')[:128] for part in parts)

        # setup defaults (user-defined take precedence)
        # root search paths
        lyric_paths = \
            config.getstringlist("memory", "lyric_rootpaths", [])
        # ensure default paths
        lyric_paths.append(os.path.join(get_home_dir(), ".lyrics"))
        lyric_paths.append(
            os.path.join(os.path.dirname(self.comma('~filename'))))
        # search pathfile names
        lyric_filenames = \
            config.getstringlist("memory", "lyric_filenames", [])
        # ensure some default pathfile names
        lyric_filenames.append(
            sanitise(os.sep, [(self.comma("lyricist") or self.comma("artist")),
                              self.comma("title")]) + u'.lyric')
        lyric_filenames.append(
            sanitise(' - ', [(self.comma("lyricist") or self.comma("artist")),
                             self.comma("title")]) + u'.lyric')

        # generate all potential paths (unresolved/unexpanded)
        pathfiles = OrderedDict()
        for r in lyric_paths:
            for f in lyric_filenames:
                pathfile = os.path.join(r, os.path.dirname(f),
                                        fsnative(os.path.basename(f)))
                rpf = RootPathFile(r, pathfile)
                if not pathfile in pathfiles:
                    pathfiles[pathfile] = rpf

        #print_d("searching for lyrics in:\n%s" % '\n'.join(pathfiles.keys()))

        # expand each raw pathfile in turn and test for existence
        match_ = ""
        pathfiles_expanded = OrderedDict()
        for pf, rpf in pathfiles.items():
            for rpf in expand_pathfile(rpf):  # resolved as late as possible
                pathfile = rpf.pathfile
                pathfiles_expanded[pathfile] = rpf
                if os.path.exists(pathfile):
                    match_ = pathfile
                    break
            if match_ != "":
                break

        if not match_:
            # search even harder!
            lyric_extensions = ['lyric', 'lyrics', '', 'txt']

            #print_d("extending search to extensions: %s" % lyric_extensions)

            def generate_mod_ext_paths(pathfile):
                # separate pathfile's extension (if any)
                ext = os.path.splitext(pathfile)[1][1:]
                path = pathfile[:-1 * len(ext)].strip('.') if ext else pathfile
                # skip the proposed lyric extension if it is the same as
                # the original for a given search pathfile stub - it has
                # already been tested without success!
                extra_extensions = [x for x in lyric_extensions if x != ext]

                # join valid new extensions to pathfile stub and return
                return [
                    '.'.join([path, ext]) if ext else path
                    for ext in extra_extensions
                ]

            # look for a match by modifying the extension for each of the
            # (now fully resolved) 'pathfiles_expanded' search items
            for pathfile in pathfiles_expanded.keys():
                # get alternatives for existence testing
                paths_mod_ext = generate_mod_ext_paths(pathfile)
                for path_ext in paths_mod_ext:
                    if os.path.exists(path_ext):
                        # persistence has paid off!
                        #print_d("extended search match!")
                        match_ = path_ext
                        break
                if match_:
                    break

        if not match_:
            # default
            match_ = list(pathfiles_expanded.keys())[0]

        return match_
Beispiel #16
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super().__init__(dialog=False)

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(600, 480)

        main_box = Gtk.VBox()
        self.add(main_box)
        self.side_book = qltk.Notebook()

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after('drag-data-received',
                                    self.__songlist_drag_data_recv)
        self.song_scroller = ScrolledWindow()
        self.song_scroller.set_policy(Gtk.PolicyType.AUTOMATIC,
                                      Gtk.PolicyType.AUTOMATIC)
        self.song_scroller.set_shadow_type(Gtk.ShadowType.IN)
        self.song_scroller.add(self.songlist)

        self.qexpander = QueueExpander(library, player)
        self.qexpander.set_no_show_all(True)
        self.qexpander.set_visible(config.getboolean("memory", "queue"))

        def on_queue_visible(qex, param):
            config.set("memory", "queue", str(qex.get_visible()))

        self.qexpander.connect("notify::visible", on_queue_visible)

        self.playlist = PlaylistMux(player, self.qexpander.model,
                                    self.songlist.model)

        self.__player = player
        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, None, True)

        keyval, mod = Gtk.accelerator_parse("<Primary><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        self.__paned = paned = ConfigRHPaned("memory", "sidebar_pos", 0.25)
        paned.pack1(self.__browserbox, resize=True)
        # We'll pack2 when necessary (when the first sidebar plugin is set up)

        main_box.pack_start(paned, True, True, 0)

        play_order = PlayOrderWidget(self.songlist.model, player)
        statusbox = StatusBarBox(play_order, self.qexpander)
        self.order = play_order
        self.statusbar = statusbox.statusbar

        main_box.pack_start(Align(statusbox, border=3, top=-3), False, True, 0)

        self.songpane = SongListPaned(self.song_scroller, self.qexpander)
        self.songpane.show_all()

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self._select_browser(self, config.get("memory", "browser"),
                                 library, player, restore_browser)
        except:
            config.set("memory", "browser", browsers.name(browsers.default))
            config.save()
            raise

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_totals)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Beispiel #17
0
    def _moveart(self, art_sets, pathfile_old, pathfile_new, song):

        path_old = os.path.dirname(os.path.realpath(pathfile_old))
        path_new = os.path.dirname(os.path.realpath(pathfile_new))
        if os.path.realpath(path_old) == os.path.realpath(path_new):
            return
        if (path_old in art_sets.keys() and not art_sets[path_old]):
            return

        # get art set for path
        images = []
        if path_old in art_sets.keys():
            images = art_sets[path_old]
        else:
            def glob_escape(s):
                for c in '[*?':
                    s = s.replace(c, '[' + c + ']')
                return s

            # generate art set for path
            art_sets[path_old] = images
            path_old_escaped = glob_escape(path_old)
            for suffix in self.IMAGE_EXTENSIONS:
                images.extend(glob.glob(os.path.join(path_old_escaped,
                                                     "*." + suffix)))
        if images:
            # set not empty yet, (re)process
            filenames = config.getstringlist("albumart", "search_filenames")
            moves = []
            for fn in filenames:
                fn = os.path.join(path_old, fn)
                if "<" in fn:
                    # resolve path
                    fnres = ArbitraryExtensionFileFromPattern(fn).format(song)
                    if fnres in images and fnres not in moves:
                        moves.append(fnres)
                elif "*" in fn:
                    moves.extend(f for f in glob.glob(fn)
                                     if f in images and f not in moves)
                elif fn in images and fn not in moves:
                    moves.append(fn)
            if len(moves) > 0:
                overwrite = config.getboolean("rename", "move_art_overwrite")
                for fnmove in moves:
                    try:
                        # existing files safeguarded until move successful,
                        # then deleted if overwrite set
                        fnmoveto = os.path.join(path_new,
                                                os.path.split(fnmove)[1])
                        fnmoveto_orig = ""
                        if os.path.exists(fnmoveto):
                            fnmoveto_orig = fnmoveto + ".orig"
                            if not os.path.exists(fnmoveto_orig):
                                os.rename(fnmoveto, fnmoveto_orig)
                            else:
                                suffix = 1
                                while os.path.exists(fnmoveto_orig +
                                                     "." + str(suffix)):
                                    suffix += 1
                                fnmoveto_orig = (fnmoveto_orig +
                                                 "." + str(suffix))
                                os.rename(fnmoveto, fnmoveto_orig)
                        print_d("Renaming image %r to %r" %
                                   (fnmove, fnmoveto), self)
                        shutil.move(fnmove, fnmoveto)
                        if overwrite and fnmoveto_orig:
                            os.remove(fnmoveto_orig)
                        images.remove(fnmove)
                    except Exception:
                        util.print_exc()
Beispiel #18
0
    def __init__(self, library, player, headless=False):
        super(QuodLibetWindow, self).__init__(dialog=False)
        self.last_dir = const.HOME

        self.__update_title(player)
        self.set_default_size(550, 450)

        main_box = Gtk.VBox()
        self.add(main_box)

        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        # dbus app menu
        AppMenu(self, ui.get_action_groups()[0])

        accel_fn = os.path.join(const.USERDIR, "accels")
        Gtk.AccelMap.load(accel_fn)

        def accel_save_cb(*args):
            Gtk.AccelMap.save(accel_fn)
        accel_group.connect_object('accel-changed', accel_save_cb, None)
        main_box.pack_start(ui.get_widget("/Menu"), False, True, 0)

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.show_all()
        self.songlist.connect_after(
            'drag-data-received', self.__songlist_drag_data_recv)
        self.song_scroller = SongListScroller(
            ui.get_widget("/Menu/View/SongList"))
        self.song_scroller.add(self.songlist)
        self.qexpander = QueueExpander(
            ui.get_widget("/Menu/View/Queue"), library, player)
        self.playlist = PlaylistMux(
            player, self.qexpander.model, self.songlist.model)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Alignment(bottom=3)
        main_box.pack_start(self.__browserbox, True, True, 0)

        statusbox = StatusBarBox(self.songlist.model, player)
        self.order = statusbox.order
        self.repeat = statusbox.repeat
        self.statusbar = statusbox.statusbar

        main_box.pack_start(
            Alignment(statusbox, border=3, top=-3, right=3),
            False, True, 0)

        self.songpane = ConfigRVPaned("memory", "queue_position", 0.75)
        self.songpane.pack1(self.song_scroller, resize=True, shrink=False)
        self.songpane.pack2(self.qexpander, resize=True, shrink=False)
        self.__handle_position = self.songpane.get_property("position")

        def songpane_button_press_cb(pane, event):
            """If we start to drag the pane handle while the
            queue expander is unexpanded, expand it and move the handle
            to the bottom, so we can 'drag' the queue out
            """

            if event.window != pane.get_handle_window():
                return False

            if not self.qexpander.get_expanded():
                self.qexpander.set_expanded(True)
                pane.set_relative(1.0)
            return False

        self.songpane.connect("button-press-event", songpane_button_press_cb)

        self.song_scroller.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::expanded', self.__expand_or)
        self.qexpander.connect('draw', self.__qex_size_allocate)
        self.songpane.connect('notify', self.__moved_pane_handle)

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        try:
            self.select_browser(
                self, config.get("memory", "browser"), library, player, True)
        except:
            config.set("memory", "browser", browsers.name(0))
            config.save(const.CONFIG)
            raise

        # set at least the playlist before the mainloop starts..
        player.setup(self.playlist, None, 0)

        def delayed_song_set():
            self.__delayed_setup = None
            song = library.get(config.get("memory", "song"))
            seek_pos = config.getint("memory", "seek", 0)
            config.set("memory", "seek", 0)
            player.setup(self.playlist, song, seek_pos)
        self.__delayed_setup = GLib.idle_add(delayed_song_set)
        self.showhide_playlist(ui.get_widget("/Menu/View/SongList"))
        self.showhide_playqueue(ui.get_widget("/Menu/View/Queue"))

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_time)

        lib = library.librarian
        gobject_weak(lib.connect_object, 'changed', self.__song_changed,
                     player, parent=self)

        self._playback_error_dialog = None
        player_sigs = [
            ('song-started', self.__song_started),
            ('paused', self.__update_paused, True),
            ('unpaused', self.__update_paused, False),
        ]
        for sig in player_sigs:
            gobject_weak(player.connect, *sig, **{"parent": self})

        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        gobject_weak(player.connect_after, 'error',
                     self.__player_error, **{"parent": self})

        # connect after to let SongTracker update stats
        player_sigs.append(
            gobject_weak(player.connect_after, "song-ended",
                         self.__song_ended, parent=self))

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(
            Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            GLib.idle_add(self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect_object("key-press-event", self.__key_pressed, player)

        self.connect("delete-event", self.__save_browser)
        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Beispiel #19
0
    def _moveart(self, art_sets, pathfile_old, pathfile_new, song):

        path_old = os.path.dirname(os.path.realpath(pathfile_old))
        path_new = os.path.dirname(os.path.realpath(pathfile_new))
        if os.path.realpath(path_old) == os.path.realpath(path_new):
            return
        if (path_old in art_sets.keys() and not art_sets[path_old]):
            return

        # get art set for path
        images = []
        if path_old in art_sets.keys():
            images = art_sets[path_old]
        else:

            def glob_escape(s):
                for c in '[*?':
                    s = s.replace(c, '[' + c + ']')
                return s

            # generate art set for path
            art_sets[path_old] = images
            path_old_escaped = glob_escape(path_old)
            for suffix in self.IMAGE_EXTENSIONS:
                images.extend(
                    glob.glob(os.path.join(path_old_escaped, "*." + suffix)))
        if images:
            # set not empty yet, (re)process
            filenames = config.getstringlist("albumart", "search_filenames")
            moves = []
            for fn in filenames:
                fn = os.path.join(path_old, fn)
                if "<" in fn:
                    # resolve path
                    fnres = ArbitraryExtensionFileFromPattern(fn).format(song)
                    if fnres in images and fnres not in moves:
                        moves.append(fnres)
                elif "*" in fn:
                    moves.extend(f for f in glob.glob(fn)
                                 if f in images and f not in moves)
                elif fn in images and fn not in moves:
                    moves.append(fn)
            if len(moves) > 0:
                overwrite = config.getboolean("rename", "move_art_overwrite")
                for fnmove in moves:
                    try:
                        # existing files safeguarded until move successful,
                        # then deleted if overwrite set
                        fnmoveto = os.path.join(path_new,
                                                os.path.split(fnmove)[1])
                        fnmoveto_orig = ""
                        if os.path.exists(fnmoveto):
                            fnmoveto_orig = fnmoveto + ".orig"
                            if not os.path.exists(fnmoveto_orig):
                                os.rename(fnmoveto, fnmoveto_orig)
                            else:
                                suffix = 1
                                while os.path.exists(fnmoveto_orig + "." +
                                                     str(suffix)):
                                    suffix += 1
                                fnmoveto_orig = (fnmoveto_orig + "." +
                                                 str(suffix))
                                os.rename(fnmoveto, fnmoveto_orig)
                        print_d("Renaming image %r to %r" % (fnmove, fnmoveto),
                                self)
                        shutil.move(fnmove, fnmoveto)
                        if overwrite and fnmoveto_orig:
                            os.remove(fnmoveto_orig)
                        images.remove(fnmove)
                    except Exception:
                        util.print_exc()
Beispiel #20
0
 def config_get_stringlist(cls, name, default=False):
     """Gets a config string list for this plugin"""
     return config.getstringlist(PM.CONFIG_SECTION, cls._config_key(name),
                              default)
Beispiel #21
0
    def lyric_filename(self):
        """Returns the validated, or default, lyrics filename for this
        file. User defined '[memory] lyric_rootpaths' and
        '[memory] lyric_filenames' matches take precedence"""

        from quodlibet.pattern \
            import ArbitraryExtensionFileFromPattern as expand_patterns

        rx_params = re.compile(r'[^\\]<[^' + re.escape(os.sep) + r']*[^\\]>')

        def expand_pathfile(rpf):
            """Return the expanded RootPathFile"""
            expanded = []
            root = expanduser(rpf.root)
            pathfile = expanduser(rpf.pathfile)
            if rx_params.search(pathfile):
                root = expand_patterns(root).format(self)
                pathfile = expand_patterns(pathfile).format(self)
            rpf = RootPathFile(root, pathfile)
            expanded.append(rpf)
            if not os.path.exists(pathfile) and is_windows():
                # prioritise a special character encoded version
                #
                # most 'alien' chars are supported for 'nix fs paths, and we
                # only pass the proposed path through 'escape_filename' (which
                # apparently doesn't respect case) if we don't care about case!
                #
                # FIX: assumes 'nix build used on a case-sensitive fs, nt case
                # insensitive. clearly this is not biting anyone though (yet!)
                pathfile = os.path.sep.join([rpf.root, rpf.end_escaped])
                rpf = RootPathFile(rpf.root, pathfile)
                expanded.insert(len(expanded) - 1, rpf)
            return expanded

        def sanitise(sep, parts):
            """Return a santisied version of a path's parts"""
            return sep.join(part.replace(os.path.sep, u'')[:128]
                                for part in parts)

        # setup defaults (user-defined take precedence)
        # root search paths
        lyric_paths = \
            config.getstringlist("memory", "lyric_rootpaths", [])
        # ensure default paths
        lyric_paths.append(os.path.join(get_home_dir(), ".lyrics"))
        lyric_paths.append(
            os.path.join(os.path.dirname(self.comma('~filename'))))
        # search pathfile names
        lyric_filenames = \
            config.getstringlist("memory", "lyric_filenames", [])
        # ensure some default pathfile names
        lyric_filenames.append(
            sanitise(os.sep, [(self.comma("lyricist") or
                              self.comma("artist")),
                              self.comma("title")]) + u'.lyric')
        lyric_filenames.append(
            sanitise(' - ', [(self.comma("lyricist") or
                             self.comma("artist")),
                             self.comma("title")]) + u'.lyric')

        # generate all potential paths (unresolved/unexpanded)
        pathfiles = OrderedDict()
        for r in lyric_paths:
            for f in lyric_filenames:
                pathfile = os.path.join(r, os.path.dirname(f),
                                        fsnative(os.path.basename(f)))
                rpf = RootPathFile(r, pathfile)
                if not pathfile in pathfiles:
                    pathfiles[pathfile] = rpf

        #print_d("searching for lyrics in:\n%s" % '\n'.join(pathfiles.keys()))

        # expand each raw pathfile in turn and test for existence
        match_ = ""
        pathfiles_expanded = OrderedDict()
        for pf, rpf in pathfiles.items():
            for rpf in expand_pathfile(rpf):  # resolved as late as possible
                pathfile = rpf.pathfile
                pathfiles_expanded[pathfile] = rpf
                if os.path.exists(pathfile):
                    match_ = pathfile
                    break
            if match_ != "":
                break

        if not match_:
            # search even harder!
            lyric_extensions = ['lyric', 'lyrics', '', 'txt']
            #print_d("extending search to extensions: %s" % lyric_extensions)

            def generate_mod_ext_paths(pathfile):
                # separate pathfile's extension (if any)
                ext = os.path.splitext(pathfile)[1][1:]
                path = pathfile[:-1 * len(ext)].strip('.') if ext else pathfile
                # skip the proposed lyric extension if it is the same as
                # the original for a given search pathfile stub - it has
                # already been tested without success!
                extra_extensions = [x for x in lyric_extensions if x != ext]

                # join valid new extensions to pathfile stub and return
                return ['.'.join([path, ext]) if ext else path
                           for ext in extra_extensions]

            # look for a match by modifying the extension for each of the
            # (now fully resolved) 'pathfiles_expanded' search items
            for pathfile in pathfiles_expanded.keys():
                # get alternatives for existence testing
                paths_mod_ext = generate_mod_ext_paths(pathfile)
                for path_ext in paths_mod_ext:
                    if os.path.exists(path_ext):
                        # persistence has paid off!
                        #print_d("extended search match!")
                        match_ = path_ext
                        break
                if match_:
                    break

        if not match_:
            # default
            match_ = list(pathfiles_expanded.keys())[0]

        return match_
    def __init__(self, library, player, headless=False):
        super(QuodLibetWindow, self).__init__(dialog=False)
        self.last_dir = const.HOME

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(550, 450)

        main_box = Gtk.VBox()
        self.add(main_box)

        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, True)

        keyval, mod = Gtk.accelerator_parse("<control><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # dbus app menu
        AppMenu(self, ui.get_action_groups()[0])

        # custom accel map
        accel_fn = os.path.join(const.USERDIR, "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.show_all()
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after(
            'drag-data-received', self.__songlist_drag_data_recv)
        self.song_scroller = SongListScroller(
            ui.get_widget("/Menu/View/SongList"))
        self.song_scroller.add(self.songlist)
        self.qexpander = QueueExpander(
            ui.get_widget("/Menu/View/Queue"), library, player)
        self.playlist = PlaylistMux(
            player, self.qexpander.model, self.songlist.model)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Alignment(bottom=3)
        main_box.pack_start(self.__browserbox, True, True, 0)

        statusbox = StatusBarBox(self.songlist.model, player)
        self.order = statusbox.order
        self.repeat = statusbox.repeat
        self.statusbar = statusbox.statusbar

        main_box.pack_start(
            Alignment(statusbox, border=3, top=-3, right=3),
            False, True, 0)

        self.songpane = ConfigRVPaned("memory", "queue_position", 0.75)
        self.songpane.pack1(self.song_scroller, resize=True, shrink=False)
        self.songpane.pack2(self.qexpander, resize=True, shrink=False)
        self.__handle_position = self.songpane.get_property("position")

        def songpane_button_press_cb(pane, event):
            """If we start to drag the pane handle while the
            queue expander is unexpanded, expand it and move the handle
            to the bottom, so we can 'drag' the queue out
            """

            if event.window != pane.get_handle_window():
                return False

            if not self.qexpander.get_expanded():
                self.qexpander.set_expanded(True)
                pane.set_relative(1.0)
            return False

        self.songpane.connect("button-press-event", songpane_button_press_cb)

        self.song_scroller.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::visible', self.__show_or)
        self.qexpander.connect('notify::expanded', self.__expand_or)
        self.qexpander.connect('draw', self.__qex_size_allocate)
        self.songpane.connect('notify', self.__moved_pane_handle)

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self.select_browser(
                self, config.get("memory", "browser"), library, player,
                restore_browser)
        except:
            config.set("memory", "browser", browsers.name(0))
            config.save(const.CONFIG)
            raise

        self.showhide_playlist(ui.get_widget("/Menu/View/SongList"))
        self.showhide_playqueue(ui.get_widget("/Menu/View/Queue"))

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_time)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        self._playback_error_dialog = None
        player_sigs = [
            ('song-started', self.__song_started),
            ('paused', self.__update_paused, True),
            ('unpaused', self.__update_paused, False),
        ]
        for sig in player_sigs:
            connect_destroy(player, *sig)

        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)

        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(
            Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            GLib.idle_add(self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")
Beispiel #23
0
    def __init__(self, library, player, headless=False, restore_cb=None):
        super(QuodLibetWindow, self).__init__(dialog=False)

        self.__destroyed = False
        self.__update_title(player)
        self.set_default_size(600, 480)

        main_box = Gtk.VBox()
        self.add(main_box)
        self.side_book = qltk.Notebook()

        # get the playlist up before other stuff
        self.songlist = MainSongList(library, player)
        self.songlist.connect("key-press-event", self.__songlist_key_press)
        self.songlist.connect_after(
            'drag-data-received', self.__songlist_drag_data_recv)
        self.song_scroller = ScrolledWindow()
        self.song_scroller.set_policy(
            Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.song_scroller.set_shadow_type(Gtk.ShadowType.IN)
        self.song_scroller.add(self.songlist)

        self.qexpander = QueueExpander(library, player)
        self.qexpander.set_no_show_all(True)
        self.qexpander.set_visible(config.getboolean("memory", "queue"))

        def on_queue_visible(qex, param):
            config.set("memory", "queue", str(qex.get_visible()))

        self.qexpander.connect("notify::visible", on_queue_visible)

        self.playlist = PlaylistMux(
            player, self.qexpander.model, self.songlist.model)

        self.__player = player
        # create main menubar, load/restore accelerator groups
        self.__library = library
        ui = self.__create_menu(player, library)
        accel_group = ui.get_accel_group()
        self.add_accel_group(accel_group)

        def scroll_and_jump(*args):
            self.__jump_to_current(True, None, True)

        keyval, mod = Gtk.accelerator_parse("<Primary><shift>J")
        accel_group.connect(keyval, mod, 0, scroll_and_jump)

        # custom accel map
        accel_fn = os.path.join(quodlibet.get_user_dir(), "accels")
        Gtk.AccelMap.load(accel_fn)
        # save right away so we fill the file with example comments of all
        # accels
        Gtk.AccelMap.save(accel_fn)

        menubar = ui.get_widget("/Menu")

        # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79
        # toplevel menu items show an empty 16x16 image. While we don't
        # need image items there UIManager creates them by default.
        # Work around by removing the empty GtkImages
        for child in menubar.get_children():
            if isinstance(child, Gtk.ImageMenuItem):
                child.set_image(None)

        main_box.pack_start(menubar, False, True, 0)

        top_bar = TopBar(self, player, library)
        main_box.pack_start(top_bar, False, True, 0)
        self.top_bar = top_bar

        self.__browserbox = Align(bottom=3)
        self.__paned = paned = ConfigRHPaned("memory", "sidebar_pos", 0.25)
        paned.pack1(self.__browserbox, resize=True)
        # We'll pack2 when necessary (when the first sidebar plugin is set up)

        main_box.pack_start(paned, True, True, 0)

        play_order = PlayOrderWidget(self.songlist.model, player)
        statusbox = StatusBarBox(play_order, self.qexpander)
        self.order = play_order
        self.statusbar = statusbox.statusbar

        main_box.pack_start(
            Align(statusbox, border=3, top=-3),
            False, True, 0)

        self.songpane = SongListPaned(self.song_scroller, self.qexpander)
        self.songpane.show_all()

        try:
            orders = []
            for e in config.getstringlist('memory', 'sortby', []):
                orders.append((e[1:], int(e[0])))
        except ValueError:
            pass
        else:
            self.songlist.set_sort_orders(orders)

        self.browser = None
        self.ui = ui

        main_box.show_all()

        self._playback_error_dialog = None
        connect_destroy(player, 'song-started', self.__song_started)
        connect_destroy(player, 'paused', self.__update_paused, True)
        connect_destroy(player, 'unpaused', self.__update_paused, False)
        # make sure we redraw all error indicators before opening
        # a dialog (blocking the main loop), so connect after default handlers
        connect_after_destroy(player, 'error', self.__player_error)
        # connect after to let SongTracker update stats
        connect_after_destroy(player, "song-ended", self.__song_ended)

        # set at least the playlist. the song should be restored
        # after the browser emits the song list
        player.setup(self.playlist, None, 0)
        self.__restore_cb = restore_cb
        self.__first_browser_set = True

        restore_browser = not headless
        try:
            self._select_browser(
                self, config.get("memory", "browser"), library, player,
                restore_browser)
        except:
            config.set("memory", "browser", browsers.name(browsers.default))
            config.save()
            raise

        self.songlist.connect('popup-menu', self.__songs_popup_menu)
        self.songlist.connect('columns-changed', self.__cols_changed)
        self.songlist.connect('columns-changed', self.__hide_headers)
        self.songlist.info.connect("changed", self.__set_totals)

        lib = library.librarian
        connect_destroy(lib, 'changed', self.__song_changed, player)

        targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]

        self.drag_dest_set(
            Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        self.connect('drag-data-received', self.__drag_data_received)

        if not headless:
            on_first_map(self, self.__configure_scan_dirs, library)

        if config.getboolean('library', 'refresh_on_start'):
            self.__rebuild(None, False)

        self.connect("key-press-event", self.__key_pressed, player)

        self.connect("destroy", self.__destroy)

        self.enable_window_tracking("quodlibet")