1# Copyright (c) 2017-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
2# This program is free software: you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation, either version 3 of the License, or
5# (at your option) any later version.
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU General Public License for more details.
10# You should have received a copy of the GNU General Public License
11# along with this program. If not, see <http://www.gnu.org/licenses/>.
12
13from gi.repository import Gtk, Gdk, GLib, WebKit2
14
15from eolie.sites_manager_child import SitesManagerChild
16from eolie.define import App, LoadingType, MARGIN_SMALL
17from eolie.utils import get_safe_netloc, update_popover_internals
18
19
20class SitesManager(Gtk.Grid):
21    """
22        Site manager (merged netloc of opened pages)
23    """
24
25    def __init__(self, window):
26        """
27            Init stack
28            @param window as Window
29        """
30        Gtk.Grid.__init__(self)
31        self.set_orientation(Gtk.Orientation.VERTICAL)
32        self.__window = window
33        self.__initial_sort = []
34        self.__show_labels = False
35        self.set_property("width-request", 50)
36        self.get_style_context().add_class("sidebar")
37        self.__scrolled = Gtk.ScrolledWindow()
38        self.__scrolled.set_policy(Gtk.PolicyType.NEVER,
39                                   Gtk.PolicyType.AUTOMATIC)
40        self.__scrolled.set_vexpand(True)
41        self.__scrolled.set_hexpand(True)
42        self.__scrolled.show()
43        viewport = Gtk.Viewport()
44        viewport.show()
45        self.__scrolled.add(viewport)
46        self.set_hexpand(False)
47
48        self.__box = Gtk.ListBox.new()
49        self.__box.set_activate_on_single_click(True)
50        self.__box.set_selection_mode(Gtk.SelectionMode.NONE)
51        self.__box.set_margin_start(2)
52        self.__box.set_margin_end(2)
53        self.__box.set_margin_top(2)
54        self.__box.set_margin_bottom(2)
55        self.__box.show()
56        self.__box.connect("row-activated", self.__on_row_activated)
57
58        viewport.set_property("valign", Gtk.Align.START)
59        viewport.add(self.__box)
60
61        menu_button = Gtk.Button.new_from_icon_name(
62            "view-more-horizontal-symbolic", Gtk.IconSize.BUTTON)
63        menu_button.show()
64        menu_button.get_style_context().add_class("overlay-button")
65        menu_button.set_property("margin", MARGIN_SMALL)
66        menu_button.connect("clicked", self.__on_menu_button_clicked)
67
68        self.add(self.__scrolled)
69        self.add(menu_button)
70
71    def add_webview(self, webview):
72        """
73            Add a new web view to monitor
74            @param webview as WebView
75        """
76        # Force update
77        if webview.uri:
78            self.__on_webview_load_changed(webview,
79                                           WebKit2.LoadEvent.COMMITTED)
80        webview.connect("load-changed", self.__on_webview_load_changed)
81        webview.connect("destroy", self.__on_webview_destroy)
82
83    def remove_webview(self, webview):
84        """
85            Remove web view from pages manager
86            @param webview as WebView
87        """
88        count = len(self.__box.get_children())
89        for site in self.__box.get_children():
90            site.remove_webview(webview)
91            if site.empty and count > 1:
92                site.destroy()
93        webview.disconnect_by_func(self.__on_webview_load_changed)
94        webview.disconnect_by_func(self.__on_webview_destroy)
95
96    def next(self):
97        """
98            Show next site
99        """
100        current = None
101        children = self.__box.get_children()
102        for child in children:
103            if child.get_state_flags() & Gtk.StateFlags.VISITED:
104                current = child
105            child.unset_state_flags(Gtk.StateFlags.VISITED)
106        index = current.get_index()
107        if index + 1 < len(children):
108            next_row = self.__box.get_row_at_index(index + 1)
109        else:
110            next_row = self.__box.get_row_at_index(0)
111        if next_row is not None:
112            next_row.set_state_flags(Gtk.StateFlags.VISITED, False)
113            self.__window.container.set_visible_webview(next_row.webviews[0])
114            if len(next_row.webviews) == 1:
115                self.__window.container.set_expose(False)
116            else:
117                self.__window.container.pages_manager.set_filter(
118                    next_row.netloc)
119                self.__window.container.set_expose(True)
120
121    def previous(self):
122        """
123            Show previous site
124        """
125        current = None
126        children = self.__box.get_children()
127        for child in children:
128            if child.get_state_flags() & Gtk.StateFlags.VISITED:
129                current = child
130            child.unset_state_flags(Gtk.StateFlags.VISITED)
131        index = current.get_index()
132        if index == 0:
133            next_row = self.__box.get_row_at_index(len(children) - 1)
134        else:
135            next_row = self.__box.get_row_at_index(index - 1)
136        if next_row is not None:
137            next_row.set_state_flags(Gtk.StateFlags.VISITED, False)
138            self.__window.container.set_visible_webview(next_row.webviews[0])
139            if len(next_row.webviews) == 1:
140                self.__window.container.set_expose(False)
141            else:
142                self.__window.container.pages_manager.set_filter(
143                    next_row.netloc)
144                self.__window.container.set_expose(True)
145
146    def update_visible_child(self):
147        """
148            Mark current child as visible
149            Unmark all others
150        """
151        current = self.__window.container.webview
152        for child in self.__box.get_children():
153            if current in child.webviews:
154                child.set_state_flags(Gtk.StateFlags.VISITED, False)
155                child.update_favicon()
156                # Wait loop empty: will fails otherwise if child just created
157                GLib.idle_add(self.__scroll_to_child, child)
158            else:
159                child.unset_state_flags(Gtk.StateFlags.VISITED)
160
161    def set_initial_sort(self, sort):
162        """
163            Set initial site sort
164            @param sort as [str]
165        """
166        if sort:
167            self.__box.set_sort_func(self.__sort_func)
168        else:
169            self.__box.set_sort_func(None)
170        self.__initial_sort = sort
171
172    def update_shown_state(self, webview):
173        """
174            Update shown state for webview
175            @param webview as WebView
176        """
177        for child in self.__box.get_children():
178            for _webview in child.webviews:
179                if _webview == webview:
180                    child.indicator_label.mark(webview)
181                    return
182
183    def show_labels(self, show):
184        """
185            Show labels on children
186            @param show as bool
187        """
188        self.__show_labels = show
189        for child in self.__box.get_children():
190            child.show_label(show)
191
192    @property
193    def sort(self):
194        """
195            Get current sort
196            @return [str]
197        """
198        sort = []
199        for child in self.__box.get_children():
200            sort.append(child.netloc)
201        return sort
202
203#######################
204# PRIVATE             #
205#######################
206    def __sort_func(self, row1, row2):
207        """
208            Sort rows based on inital sort
209            @param row1 as Gtk.ListBoxRow
210            @param row2 as Gtk.ListBoxRow
211        """
212        try:
213            index1 = self.__initial_sort.index(row1.netloc)
214            index2 = self.__initial_sort.index(row2.netloc)
215            return index1 > index2
216        except:
217            return False
218
219    def __get_index(self, netloc):
220        """
221            Get child index
222            @param netloc as str
223            @return int
224        """
225        # Search current index
226        children = self.__box.get_children()
227        index = 0
228        for child in children:
229            if child.netloc == netloc:
230                break
231            index += 1
232        return index
233
234    def __scroll_to_child(self, child):
235        """
236            Scroll to child
237            @param child as SitesManagerChild
238        """
239        adj = self.__scrolled.get_vadjustment()
240        if adj is None:
241            return
242        value = adj.get_value()
243        coordinates = child.translate_coordinates(self.__box, 0, 0)
244        if coordinates is None:
245            return
246        y = coordinates[1]
247        if y + child.get_allocated_height() >\
248                self.__scrolled.get_allocated_height() + value or\
249                y - child.get_allocated_height() < 0 + value:
250            self.__scrolled.get_vadjustment().set_value(y)
251
252    def __on_webview_destroy(self, webview):
253        """
254            Clean children
255            @param webview as WebView
256        """
257        self.remove_webview(webview)
258
259    def __on_webview_load_changed(self, webview, event):
260        """
261            Update children
262            @param webview as WebView
263            @param event as WebKit2.LoadEvent
264        """
265        if event != WebKit2.LoadEvent.COMMITTED:
266            return
267        netloc = get_safe_netloc(webview.uri)
268        child = None
269        empty_child = None
270        # Do not group by netloc
271        if webview.is_ephemeral:
272            for site in self.__box.get_children():
273                if site.is_ephemeral:
274                    child = site
275                    break
276        else:
277            # Search for a child for wanted netloc
278            # Clean up any child matching webview, allowing us to reuse it
279            for site in self.__box.get_children():
280                if site.netloc == netloc and site.is_ephemeral is False:
281                    child = site
282                else:
283                    site.remove_webview(webview)
284                    if site.empty:
285                        empty_child = site
286
287        if child is None:
288            # We need to create a new child
289            if empty_child is None:
290                child = SitesManagerChild(netloc,
291                                          self.__window,
292                                          webview.is_ephemeral)
293                child.show()
294                child.add_webview(webview)
295                child.show_label(self.__show_labels)
296                self.__box.add(child)
297                self.update_visible_child()
298            # Use empty child
299            else:
300                child = empty_child
301                child.reset(netloc)
302                child.add_webview(webview)
303        # We already have a child for this netloc
304        else:
305            # Webview previous child is empty, destroy it
306            if empty_child is not None:
307                empty_child.destroy()
308            child.add_webview(webview)
309            self.update_visible_child()
310        # Webview really loaded
311        if webview.get_uri() is not None:
312            child.on_webview_load_changed(webview, event)
313
314    def __on_row_activated(self, listbox, child):
315        """
316            Show wanted expose
317            @param listbox as Gtk.ListBox
318            @param child as SitesManagerChild
319        """
320        webviews = child.webviews
321        if len(webviews) == 1:
322            self.__window.container.set_visible_webview(webviews[0])
323        else:
324            from eolie.pages_manager_list import PagesManagerList
325            widget = PagesManagerList(self.__window)
326            widget.show()
327            widget.populate(webviews)
328            popover = Gtk.Popover.new(child)
329            popover.set_modal(False)
330            self.__window.register(popover)
331            popover.get_style_context().add_class("box-shadow")
332            popover.set_position(Gtk.PositionType.RIGHT)
333            popover.add(widget)
334            popover.popup()
335
336    def __on_button_press(self, widget, event):
337        """
338            Hide popover if visible
339            @param widget as Gtk.Widget
340            @param event as Gdk.EventButton
341        """
342        if event.type == Gdk.EventType._2BUTTON_PRESS:
343            self.__window.container.add_webview(App().start_page,
344                                                LoadingType.FOREGROUND)
345        return self.__window.close_popovers()
346
347    def __on_menu_button_clicked(self, button):
348        """
349            Show pages menu
350            @param button as Gtk.Button
351        """
352        self.__window.close_popovers()
353        popover = Gtk.Popover.new_from_model(button, App().pages_menu)
354        popover.set_modal(False)
355        self.__window.register(popover)
356        popover.forall(update_popover_internals)
357        popover.popup()
358