1# Copyright 2019 The GNOME Music Developers
2#
3# GNOME Music is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# GNOME Music is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16#
17# The GNOME Music authors hereby grant permission for non-GPL compatible
18# GStreamer plugins to be used and distributed together with GStreamer
19# and GNOME Music.  This permission is above and beyond the permissions
20# granted by the GPL license by which GNOME Music is covered.  If you
21# modify this code, you may extend this exception to your version of the
22# code, but you are not obligated to do so.  If you do not wish to do so,
23# delete this exception statement from your version.
24
25import math
26
27from gettext import gettext as _
28from gi.repository import Gdk, GLib, GObject, Gtk
29
30from gnomemusic.widgets.headerbar import HeaderBar
31from gnomemusic.widgets.albumcover import AlbumCover
32from gnomemusic.widgets.albumwidget import AlbumWidget
33
34
35@Gtk.Template(resource_path="/org/gnome/Music/ui/AlbumsView.ui")
36class AlbumsView(Gtk.Stack):
37    """Gridlike view of all albums
38
39    Album activation switches to AlbumWidget.
40    """
41
42    __gtype_name__ = "AlbumsView"
43
44    icon_name = GObject.Property(
45        type=str, default="media-optical-cd-audio-symbolic",
46        flags=GObject.ParamFlags.READABLE)
47    search_mode_active = GObject.Property(type=bool, default=False)
48    selection_mode = GObject.Property(type=bool, default=False)
49    title = GObject.Property(
50        type=str, default=_("Albums"), flags=GObject.ParamFlags.READABLE)
51
52    _album_scrolled_window = Gtk.Template.Child()
53    _scrolled_window = Gtk.Template.Child()
54    _flowbox = Gtk.Template.Child()
55    _flowbox_long_press = Gtk.Template.Child()
56
57    def __init__(self, application):
58        """Initialize AlbumsView
59
60        :param application: The Application object
61        """
62        super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE)
63
64        self.props.name = "albums"
65
66        self._application = application
67        self._window = application.props.window
68        self._headerbar = self._window._headerbar
69        self._adjustment_timeout_id = 0
70        self._viewport = self._scrolled_window.get_child()
71        self._widget_counter = 1
72        self._ctrl_hold = False
73
74        model = self._application.props.coremodel.props.albums_sort
75        self._flowbox.bind_model(model, self._create_widget)
76        self._flowbox.set_hadjustment(self._scrolled_window.get_hadjustment())
77        self._flowbox.set_vadjustment(self._scrolled_window.get_vadjustment())
78        self._flowbox.connect("child-activated", self._on_child_activated)
79
80        self.bind_property(
81            "selection-mode", self._window, "selection-mode",
82            GObject.BindingFlags.DEFAULT)
83
84        self._window.connect(
85            "notify::selection-mode", self._on_selection_mode_changed)
86
87        self._album_widget = AlbumWidget(self._application)
88        self._album_widget.bind_property(
89            "selection-mode", self, "selection-mode",
90            GObject.BindingFlags.BIDIRECTIONAL)
91
92        self._album_scrolled_window.add(self._album_widget)
93
94        self.connect(
95            "notify::search-mode-active", self._on_search_mode_changed)
96
97        self._scrolled_window.props.vadjustment.connect(
98            "value-changed", self._on_vadjustment_changed)
99        self._scrolled_window.props.vadjustment.connect(
100            "changed", self._on_vadjustment_changed)
101
102    def _on_vadjustment_changed(self, adjustment):
103        if self._adjustment_timeout_id != 0:
104            GLib.source_remove(self._adjustment_timeout_id)
105            self._adjustment_timeout_id = 0
106
107        self._adjustment_timeout_id = GLib.timeout_add(
108            200, self._retrieve_covers, adjustment.props.value,
109            priority=GLib.PRIORITY_DEFAULT - 10)
110
111    def _retrieve_covers(self, old_adjustment):
112        adjustment = self._scrolled_window.props.vadjustment.props.value
113
114        if old_adjustment != adjustment:
115            return GLib.SOURCE_CONTINUE
116
117        first_cover = self._flowbox.get_child_at_index(0)
118        if first_cover is None:
119            return GLib.SOURCE_REMOVE
120
121        cover_size, _ = first_cover.get_allocated_size()
122        if cover_size.width == 0 or cover_size.height == 0:
123            return GLib.SOURCE_REMOVE
124
125        viewport_size, _ = self._viewport.get_allocated_size()
126
127        h_space = self._flowbox.get_column_spacing()
128        v_space = self._flowbox.get_row_spacing()
129        nr_cols = (
130            (viewport_size.width + h_space) // (cover_size.width + h_space))
131
132        top_left_cover = self._flowbox.get_child_at_index(
133            nr_cols * (adjustment // (cover_size.height + v_space)))
134
135        covers_col = math.ceil(viewport_size.width / cover_size.width)
136        covers_row = math.ceil(viewport_size.height / cover_size.height)
137
138        children = self._flowbox.get_children()
139        retrieve_list = []
140        for i, albumcover in enumerate(children):
141            if top_left_cover == albumcover:
142                retrieve_covers = covers_row * covers_col
143                retrieve_list = children[i:i + retrieve_covers]
144                break
145
146        for albumcover in retrieve_list:
147            albumcover.retrieve()
148
149        self._adjustment_timeout_id = 0
150
151        return GLib.SOURCE_REMOVE
152
153    def _on_selection_mode_changed(self, widget, data=None):
154        selection_mode = self._window.props.selection_mode
155        if (selection_mode == self.props.selection_mode
156                or self.get_parent().get_visible_child() != self):
157            return
158
159        self.props.selection_mode = selection_mode
160        if not self.props.selection_mode:
161            self.deselect_all()
162            self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE
163
164    def _on_search_mode_changed(self, klass, param):
165        if (not self.props.search_mode_active
166                and self._headerbar.props.stack.props.visible_child == self
167                and self.get_visible_child() == self._album_widget):
168            self._set_album_headerbar(self._album_widget.props.corealbum)
169
170    def _create_widget(self, corealbum):
171        album_widget = AlbumCover(corealbum)
172
173        self.bind_property(
174            "selection-mode", album_widget, "selection-mode",
175            GObject.BindingFlags.SYNC_CREATE
176            | GObject.BindingFlags.BIDIRECTIONAL)
177
178        # NOTE: Adding SYNC_CREATE here will trigger all the nested
179        # models to be created. This will slow down initial start,
180        # but will improve initial 'select all' speed.
181        album_widget.bind_property(
182            "selected", corealbum, "selected",
183            GObject.BindingFlags.BIDIRECTIONAL)
184
185        GLib.timeout_add(
186            self._widget_counter * 250, album_widget.retrieve,
187            priority=GLib.PRIORITY_LOW)
188        self._widget_counter = self._widget_counter + 1
189
190        return album_widget
191
192    def _back_button_clicked(self, widget, data=None):
193        self._headerbar.state = HeaderBar.State.MAIN
194        self.props.visible_child = self._scrolled_window
195
196    def _on_child_activated(self, widget, child, user_data=None):
197        corealbum = child.props.corealbum
198        if self.props.selection_mode:
199            return
200
201        # Update and display the album widget if not in selection mode
202        self._album_widget.props.corealbum = corealbum
203
204        self._set_album_headerbar(corealbum)
205        self.set_visible_child(self._album_scrolled_window)
206
207    def _set_album_headerbar(self, corealbum):
208        self._headerbar.props.state = HeaderBar.State.CHILD
209        self._headerbar.props.title = corealbum.props.title
210        self._headerbar.props.subtitle = corealbum.props.artist
211
212    @Gtk.Template.Callback()
213    def _on_flowbox_press_begin(self, gesture, sequence):
214        event = gesture.get_last_event(sequence)
215        ok, state = event.get_state()
216        if ((ok is True
217             and state == Gdk.ModifierType.CONTROL_MASK)
218                or self.props.selection_mode is True):
219            self._flowbox.props.selection_mode = Gtk.SelectionMode.MULTIPLE
220            if state == Gdk.ModifierType.CONTROL_MASK:
221                self._ctrl_hold = True
222
223    @Gtk.Template.Callback()
224    def _on_flowbox_press_cancel(self, gesture, sequence):
225        self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE
226
227    @Gtk.Template.Callback()
228    def _on_selected_children_changed(self, flowbox):
229        if self._flowbox.props.selection_mode == Gtk.SelectionMode.NONE:
230            return
231
232        if self.props.selection_mode is False:
233            self.props.selection_mode = True
234
235        rubberband_selection = len(self._flowbox.get_selected_children()) > 1
236        with self._application.props.coreselection.freeze_notify():
237            if (rubberband_selection
238                    and not self._ctrl_hold):
239                self.deselect_all()
240            for child in self._flowbox.get_selected_children():
241                if (self._ctrl_hold is True
242                        or not rubberband_selection):
243                    child.props.selected = not child.props.selected
244                else:
245                    child.props.selected = True
246
247        self._ctrl_hold = False
248        self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE
249
250    def _toggle_all_selection(self, selected):
251        """Selects or deselects all items.
252        """
253        with self._application.props.coreselection.freeze_notify():
254            if self.get_visible_child() == self._album_widget:
255                if selected is True:
256                    self._album_widget.select_all()
257                else:
258                    self._album_widget.deselect_all()
259            else:
260                for child in self._flowbox.get_children():
261                    child.props.selected = selected
262
263    def select_all(self):
264        self._toggle_all_selection(True)
265
266    def deselect_all(self):
267        self._toggle_all_selection(False)
268