diff options
Diffstat (limited to 'src')
156 files changed, 38331 insertions, 0 deletions
diff --git a/src/gnome-software-local-file.desktop.in b/src/gnome-software-local-file.desktop.in new file mode 100644 index 0000000..1c4cb0f --- /dev/null +++ b/src/gnome-software-local-file.desktop.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Software Install +Comment=Install selected software on the system +Categories=System; +Exec=gnome-software --local-filename %f +Terminal=false +Type=Application +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=system-software-install +StartupNotify=true +NoDisplay=true +MimeType=application/x-rpm;application/x-redhat-package-manager;application/x-deb;application/x-app-package;application/vnd.ms-cab-compressed;application/vnd.flatpak;application/vnd.flatpak.repo;application/vnd.flatpak.ref;application/vnd.snap; diff --git a/src/gnome-software-service.desktop.in b/src/gnome-software-service.desktop.in new file mode 100644 index 0000000..dc4d4d9 --- /dev/null +++ b/src/gnome-software-service.desktop.in @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Name=GNOME Software +Exec=@bindir@/gnome-software --gapplication-service +OnlyShowIn=GNOME;Unity; +NoDisplay=true diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml new file mode 100644 index 0000000..459ecf8 --- /dev/null +++ b/src/gnome-software.gresource.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/Software"> + <file preprocess="xml-stripblanks">gnome-software.ui</file> + <file preprocess="xml-stripblanks">gs-app-addon-row.ui</file> + <file preprocess="xml-stripblanks">gs-app-row.ui</file> + <file preprocess="xml-stripblanks">gs-basic-auth-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-category-page.ui</file> + <file preprocess="xml-stripblanks">gs-category-tile.ui</file> + <file preprocess="xml-stripblanks">gs-details-page.ui</file> + <file preprocess="xml-stripblanks">gs-extras-page.ui</file> + <file preprocess="xml-stripblanks">gs-feature-tile.ui</file> + <file preprocess="xml-stripblanks">gs-first-run-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-history-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-info-bar.ui</file> + <file preprocess="xml-stripblanks">gs-installed-page.ui</file> + <file preprocess="xml-stripblanks">gs-loading-page.ui</file> + <file preprocess="xml-stripblanks">gs-metered-data-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-moderate-page.ui</file> + <file preprocess="xml-stripblanks">gs-overview-page.ui</file> + <file preprocess="xml-stripblanks">gs-origin-popover-row.ui</file> + <file preprocess="xml-stripblanks">gs-popular-tile.ui</file> + <file preprocess="xml-stripblanks">gs-prefs-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-removal-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-repo-row.ui</file> + <file preprocess="xml-stripblanks">gs-repos-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-review-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-review-histogram.ui</file> + <file preprocess="xml-stripblanks">gs-review-row.ui</file> + <file preprocess="xml-stripblanks">gs-screenshot-image.ui</file> + <file preprocess="xml-stripblanks">gs-search-page.ui</file> + <file preprocess="xml-stripblanks">gs-star-widget.ui</file> + <file preprocess="xml-stripblanks">gs-summary-tile.ui</file> + <file preprocess="xml-stripblanks">gs-third-party-repo-row.ui</file> + <file preprocess="xml-stripblanks">gs-update-dialog.ui</file> + <file preprocess="xml-stripblanks">gs-updates-page.ui</file> + <file preprocess="xml-stripblanks">gs-upgrade-banner.ui</file> + <file preprocess="xml-stripblanks">org.freedesktop.PackageKit.xml</file> + <file>gtk-style.css</file> + <file>gtk-style-hc.css</file> + </gresource> +</gresources> diff --git a/src/gnome-software.ui b/src/gnome-software.ui new file mode 100644 index 0000000..0811b61 --- /dev/null +++ b/src/gnome-software.ui @@ -0,0 +1,602 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.15.2 on Thu Aug 15 17:13:59 2013 --> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <object class="GtkMenu" id="header_selection_menu"> + <property name="visible">True</property> + <child> + <object class="GtkMenuItem" id="select_all_menuitem"> + <property name="visible">True</property> + <property name="label" translatable="yes">Select All</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="select_none_menuitem"> + <property name="visible">True</property> + <property name="label" translatable="yes">Select None</property> + </object> + </child> + </object> + <object class="GtkPopoverMenu" id="account_popover"> + <property name="visible">False</property> + <child> + <object class="GtkBox" id="account_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin">6</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <menu id="primary_menu"> + <item> + <attribute name="label" translatable="yes">_Software Repositories</attribute> + <attribute name="action">app.sources</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Update Preferences</attribute> + <attribute name="action">app.prefs</attribute> + </item> + </menu> + <object class="GtkApplicationWindow" id="window_software"> + <property name="visible">False</property> + <property name="default-width">1200</property> + <property name="default-height">800</property> + <property name="title" translatable="yes">Software</property> + <property name="icon_name">org.gnome.Software</property> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHeaderBar" id="header"> + <property name="visible">True</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton" id="button_back"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="accessible"> + <object class="AtkObject" id="button_back_accessible"> + <property name="accessible-name" translatable="yes">Go back</property> + </object> + </child> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage" id="back_image"> + <property name="visible">True</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + <child type="title"> + <object class="GtkBox" id="title_box"> + <property name="visible">True</property> + <property name="hexpand">False</property> + <child> + <object class="GtkButtonBox" id="buttonbox_main"> + <property name="visible">True</property> + <property name="layout_style">center</property> + <style> + <class name="linked"/> + </style> + <child> + <object class="GtkToggleButton" id="button_explore"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes" comments="Translators: A label for a button to show all available software.">_Explore</property> + <style> + <class name="toolbar-primary-buttons-software"/> + </style> + </object> + </child> + <child> + <object class="GtkToggleButton" id="button_installed"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkBox" id="button_installed_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="halign">fill</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="button_installed_label"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="halign">center</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes" comments="Translators: A label for a button to show only software which is already installed.">_Installed</property> + <property name="mnemonic_widget">button_installed</property> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="button_installed_counter"> + <property name="visible">False</property> + <property name="width-chars">2</property> + <style> + <class name="counter-label"/> + </style> + </object> + </child> + </object> + </child> + <style> + <class name="toolbar-primary-buttons-software"/> + </style> + </object> + </child> + <child> + <object class="GtkToggleButton" id="button_updates"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkBox" id="button_updates_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="halign">fill</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="button_updates_label"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="halign">center</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes" comments="Translators: A label for a button to show only updates which are available to install.">_Updates</property> + <property name="mnemonic_widget">button_updates</property> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="button_updates_counter"> + <property name="visible">False</property> + <property name="width-chars">2</property> + <style> + <class name="counter-label"/> + </style> + </object> + </child> + </object> + </child> + <style> + <class name="toolbar-primary-buttons-software"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="application_details_header"> + <property name="visible">False</property> + <property name="selectable">False</property> + <property name="ellipsize">end</property> + <style> + <class name="title"/> + </style> + </object> + </child> + <child> + <object class="GtkMenuButton" id="header_selection_menu_button"> + <property name="visible">False</property> + <property name="popup">header_selection_menu</property> + <style> + <class name="selection-menu"/> + </style> + <child> + <object class="GtkBox" id="header_selection_box"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="header_selection_label"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkArrow" id="header_selection_arrow"> + <property name="visible">True</property> + <property name="arrow_type">down</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkToggleButton" id="search_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="accessible"> + <object class="AtkObject" id="search_button_accessible"> + <property name="accessible-name" translatable="yes">Search</property> + </object> + </child> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage" id="search_image"> + <property name="visible">True</property> + <property name="icon_name">edit-find-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child> + <object class="GtkMenuButton" id="menu_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="sensitive">True</property> + <property name="menu_model">primary_menu</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">open-menu-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + <child> + <object class="GtkBox" id="origin_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="halign">fill</property> + <property name="spacing">9</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes" comments="Translators: This is a label in the header bar, followed by a drop down to choose between different source repos">Source</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="visible">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkMenuButton" id="origin_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="sensitive">True</property> + <property name="popover">origin_popover</property> + <child> + <object class="GtkGrid"> + <property name="column-spacing">12</property> + <property name="visible">1</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <child> + <object class="GtkLabel" id="origin_button_label"> + <property name="label"></property> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="visible">1</property> + <property name="xalign">0</property> + <property name="width_chars">10</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">pan-down-symbolic</property> + <property name="pixel-size">16</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="visible">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkSearchBar" id="search_bar"> + <property name="visible">True</property> + <child> + <object class="GtkSearchEntry" id="entry_search"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + <property name="width_request">500</property> + <property name="max_length">100</property> + <property name="hexpand">True</property> + <property name="halign">center</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="vexpand">True</property> + <child type="overlay"> + <object class="GtkRevealer" id="notification_event"> + <property name="visible">True</property> + <property name="halign">GTK_ALIGN_CENTER</property> + <property name="valign">GTK_ALIGN_START</property> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="visible">True</property> + <style> + <class name="app-notification"/> + </style> + <child> + <object class="GtkLabel" id="label_events"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label">Some Title</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="max_width_chars">60</property> + <property name="margin_start">9</property> + <property name="margin_end">9</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButtonBox"> + <property name="layout_style">end</property> + <property name="visible">True</property> + <child> + <object class="GtkButton" id="button_events_sources"> + <property name="visible">False</property> + <property name="label" translatable="yes" comments="button in the info bar">Software Repositories</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_events_no_space"> + <property name="visible">False</property> + <property name="label" translatable="yes" comments="button in the info bar">Examine Disk</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_events_network_settings"> + <property name="visible">False</property> + <property name="label" translatable="yes" comments="button in the info bar">Network Settings</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_events_restart_required"> + <property name="visible">False</property> + <property name="label" translatable="yes" comments="button in the info bar">Restart Now</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_events_more_info"> + <property name="visible">False</property> + <property name="label" translatable="yes" comments="button in the info bar">More Information</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="button_events_dismiss"> + <property name="visible">True</property> + <property name="valign">start</property> + <style> + <class name="flat"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">window-close-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkInfoBar" id="metered_updates_bar"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <property name="message-type">GTK_MESSAGE_INFO</property> + <property name="show-close-button">False</property> + <property name="revealed">False</property> + <child internal-child="content_area"> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="margin_top">6</property> + <property name="margin_left">6</property> + <property name="margin_bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Automatic Updates Paused</property> + <property name="xalign">0.0</property> + <attributes> + <attribute name="weight" value="PANGO_WEIGHT_BOLD"/> + </attributes> + </object> + </child> + </object> + </child> + <child internal-child="action_area"> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="margin_right">6</property> + <child> + <object class="GtkButton" id="metered_updates_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes">Find Out _More</property> + </object> + </child> + </object> + </child> + <action-widgets> + <action-widget response="GTK_RESPONSE_OK">metered_updates_button</action-widget> + </action-widgets> + </object> + </child> + + <child> + <object class="GtkStack" id="stack_main"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <child> + <object class="GsOverviewPage" id="overview_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">overview</property> + </packing> + </child> + <child> + <object class="GsInstalledPage" id="installed_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">installed</property> + </packing> + </child> + <child> + <object class="GsModeratePage" id="moderate_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">moderate</property> + </packing> + </child> + <child> + <object class="GsLoadingPage" id="loading_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">loading</property> + </packing> + </child> + <child> + <object class="GsSearchPage" id="search_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">search</property> + </packing> + </child> + <child> + <object class="GsUpdatesPage" id="updates_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">updates</property> + </packing> + </child> + + <child> + <object class="GsDetailsPage" id="details_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">details</property> + </packing> + </child> + + <child> + <object class="GsCategoryPage" id="category_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">category</property> + </packing> + </child> + <child> + <object class="GsExtrasPage" id="extras_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">extras</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="origin_popover"> + <property name="visible">False</property> + <child> + <object class="GtkBox" id="origin_popover_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkScrolledWindow"> + <property name="propagate-natural-height">true</property> + <property name="propagate-natural-width">true</property> + <property name="max-content-height">600</property> + <property name="visible">true</property> + <child> + <object class="GtkListBox" id="origin_popover_list_box"> + <property name="selection-mode">none</property> + <property name="visible">true</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/gnome-software.xml b/src/gnome-software.xml new file mode 100644 index 0000000..5f87829 --- /dev/null +++ b/src/gnome-software.xml @@ -0,0 +1,74 @@ +<?xml version='1.0'?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> + +<refentry id="gnome-software"> + + <refentryinfo> + <title>gnome-software</title> + <productname>GNOME</productname> + <author> + <contrib>Maintainer</contrib> + <firstname>Richard</firstname> + <surname>Hughes</surname> + <email>richard@hughsie.com</email> + </author> + <copyright> + <year>2013</year> + <holder>Richard Hughes</holder> + </copyright> + </refentryinfo> + + <refmeta> + <refentrytitle>gnome-software</refentrytitle> + <manvolnum>1</manvolnum> + <refmiscinfo class="manual">User Commands</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>gnome-software</refname> + <refpurpose>Install applications</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <cmdsynopsis> + <command>gnome-software</command> + <arg choice="opt" rep="repeat">OPTION</arg> + </cmdsynopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + <para> + This manual page documents briefly the <command>gnome-software</command> command. + </para> + <para> + <command>gnome-software</command> allows you to add and remove + applications and update your system. + </para> + </refsect1> + + <refsect1> + <title>Options</title> + <variablelist> + <varlistentry> + <term><option>-?</option>, <option>--help</option></term> + <listitem><para>Prints a short help text and exits.</para></listitem> + </varlistentry> + <varlistentry> + <term><option>--version</option></term> + <listitem><para>Prints the program version and exits.</para></listitem> + </varlistentry> + <varlistentry> + <term><option>--mode</option> <replaceable>MODE</replaceable></term> + <listitem><para>Starts gnome-software in the given mode. <replaceable>MODE</replaceable> can be 'updates', 'updated', 'installed' or 'overview'. The default mode is 'overview'.</para></listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Author</title> + <para>This manual page was written by Richard Hughes <email>richard@hughsie.com</email>. + </para> + </refsect1> +</refentry> diff --git a/src/gs-app-addon-row.c b/src/gs-app-addon-row.c new file mode 100644 index 0000000..30fb4d6 --- /dev/null +++ b/src/gs-app-addon-row.c @@ -0,0 +1,273 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-app-addon-row.h" + +struct _GsAppAddonRow +{ + GtkListBoxRow parent_instance; + + GsApp *app; + GtkWidget *name_label; + GtkWidget *description_label; + GtkWidget *label; + GtkWidget *checkbox; +}; + +G_DEFINE_TYPE (GsAppAddonRow, gs_app_addon_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + PROP_ZERO, + PROP_SELECTED +}; + +static void +checkbox_toggled (GtkWidget *widget, GsAppAddonRow *row) +{ + g_object_notify (G_OBJECT (row), "selected"); +} + +/** + * gs_app_addon_row_get_summary: + * + * Return value: PangoMarkup + **/ +static GString * +gs_app_addon_row_get_summary (GsAppAddonRow *row) +{ + const gchar *tmp = NULL; + g_autofree gchar *escaped = NULL; + + /* try all these things in order */ + if (gs_app_get_state (row->app) == AS_APP_STATE_UNAVAILABLE) + tmp = gs_app_get_summary_missing (row->app); + if (tmp == NULL || (tmp != NULL && tmp[0] == '\0')) + tmp = gs_app_get_summary (row->app); + if (tmp == NULL || (tmp != NULL && tmp[0] == '\0')) + tmp = gs_app_get_description (row->app); + + escaped = g_markup_escape_text (tmp, -1); + return g_string_new (escaped); +} + +void +gs_app_addon_row_refresh (GsAppAddonRow *row) +{ + g_autoptr(GString) str = NULL; + + if (row->app == NULL) + return; + + /* join the lines */ + str = gs_app_addon_row_get_summary (row); + as_utils_string_replace (str, "\n", " "); + gtk_label_set_markup (GTK_LABEL (row->description_label), str->str); + gtk_label_set_label (GTK_LABEL (row->name_label), + gs_app_get_name (row->app)); + + /* update the state label */ + switch (gs_app_get_state (row->app)) { + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (row->label, TRUE); + gtk_label_set_label (GTK_LABEL (row->label), _("Pending")); + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_INSTALLED: + gtk_widget_set_visible (row->label, TRUE); + gtk_label_set_label (GTK_LABEL (row->label), _("Installed")); + break; + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (row->label, TRUE); + gtk_label_set_label (GTK_LABEL (row->label), _("Installing")); + break; + case AS_APP_STATE_REMOVING: + gtk_widget_set_visible (row->label, TRUE); + gtk_label_set_label (GTK_LABEL (row->label), _("Removing")); + break; + default: + gtk_widget_set_visible (row->label, FALSE); + break; + } + + /* update the checkbox */ + g_signal_handlers_block_by_func (row->checkbox, checkbox_toggled, row); + switch (gs_app_get_state (row->app)) { + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_sensitive (row->checkbox, TRUE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), TRUE); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + gtk_widget_set_sensitive (row->checkbox, TRUE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), FALSE); + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_INSTALLED: + gtk_widget_set_sensitive (row->checkbox, TRUE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), TRUE); + break; + case AS_APP_STATE_INSTALLING: + gtk_widget_set_sensitive (row->checkbox, FALSE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), TRUE); + break; + case AS_APP_STATE_REMOVING: + gtk_widget_set_sensitive (row->checkbox, FALSE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), FALSE); + break; + default: + gtk_widget_set_sensitive (row->checkbox, FALSE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), FALSE); + break; + } + g_signal_handlers_unblock_by_func (row->checkbox, checkbox_toggled, row); +} + +GsApp * +gs_app_addon_row_get_addon (GsAppAddonRow *row) +{ + g_return_val_if_fail (GS_IS_APP_ADDON_ROW (row), NULL); + return row->app; +} + +static gboolean +gs_app_addon_row_refresh_idle (gpointer user_data) +{ + GsAppAddonRow *row = GS_APP_ADDON_ROW (user_data); + + gs_app_addon_row_refresh (row); + + g_object_unref (row); + return G_SOURCE_REMOVE; +} + +static void +gs_app_addon_row_notify_props_changed_cb (GsApp *app, + GParamSpec *pspec, + GsAppAddonRow *row) +{ + g_idle_add (gs_app_addon_row_refresh_idle, g_object_ref (row)); +} + +static void +gs_app_addon_row_set_addon (GsAppAddonRow *row, GsApp *app) +{ + row->app = g_object_ref (app); + + g_signal_connect_object (row->app, "notify::state", + G_CALLBACK (gs_app_addon_row_notify_props_changed_cb), + row, 0); + gs_app_addon_row_refresh (row); +} + +static void +gs_app_addon_row_destroy (GtkWidget *object) +{ + GsAppAddonRow *row = GS_APP_ADDON_ROW (object); + + if (row->app) + g_signal_handlers_disconnect_by_func (row->app, gs_app_addon_row_notify_props_changed_cb, row); + + g_clear_object (&row->app); + + GTK_WIDGET_CLASS (gs_app_addon_row_parent_class)->destroy (object); +} + +static void +gs_app_addon_row_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsAppAddonRow *row = GS_APP_ADDON_ROW (object); + + switch (prop_id) { + case PROP_SELECTED: + gs_app_addon_row_set_selected (row, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_addon_row_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsAppAddonRow *row = GS_APP_ADDON_ROW (object); + + switch (prop_id) { + case PROP_SELECTED: + g_value_set_boolean (value, gs_app_addon_row_get_selected (row)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_addon_row_class_init (GsAppAddonRowClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = gs_app_addon_row_set_property; + object_class->get_property = gs_app_addon_row_get_property; + + widget_class->destroy = gs_app_addon_row_destroy; + + pspec = g_param_spec_boolean ("selected", NULL, NULL, + FALSE, G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_SELECTED, pspec); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-addon-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsAppAddonRow, name_label); + gtk_widget_class_bind_template_child (widget_class, GsAppAddonRow, description_label); + gtk_widget_class_bind_template_child (widget_class, GsAppAddonRow, label); + gtk_widget_class_bind_template_child (widget_class, GsAppAddonRow, checkbox); +} + +static void +gs_app_addon_row_init (GsAppAddonRow *row) +{ + gtk_widget_set_has_window (GTK_WIDGET (row), FALSE); + gtk_widget_init_template (GTK_WIDGET (row)); + + g_signal_connect (row->checkbox, "toggled", + G_CALLBACK (checkbox_toggled), row); +} + +void +gs_app_addon_row_set_selected (GsAppAddonRow *row, gboolean selected) +{ + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->checkbox), selected); +} + +gboolean +gs_app_addon_row_get_selected (GsAppAddonRow *row) +{ + return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (row->checkbox)); +} + +GtkWidget * +gs_app_addon_row_new (GsApp *app) +{ + GtkWidget *row; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + row = g_object_new (GS_TYPE_APP_ADDON_ROW, NULL); + gs_app_addon_row_set_addon (GS_APP_ADDON_ROW (row), app); + return row; +} diff --git a/src/gs-app-addon-row.h b/src/gs-app-addon-row.h new file mode 100644 index 0000000..257ef86 --- /dev/null +++ b/src/gs-app-addon-row.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_ADDON_ROW (gs_app_addon_row_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppAddonRow, gs_app_addon_row, GS, APP_ADDON_ROW, GtkListBoxRow) + +GtkWidget *gs_app_addon_row_new (GsApp *app); +void gs_app_addon_row_refresh (GsAppAddonRow *row); +void gs_app_addon_row_set_selected (GsAppAddonRow *row, + gboolean selected); +gboolean gs_app_addon_row_get_selected (GsAppAddonRow *row); +GsApp *gs_app_addon_row_get_addon (GsAppAddonRow *row); + +G_END_DECLS diff --git a/src/gs-app-addon-row.ui b/src/gs-app-addon-row.ui new file mode 100644 index 0000000..471d373 --- /dev/null +++ b/src/gs-app-addon-row.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsAppAddonRow" parent="GtkListBoxRow"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="orientation">horizontal</property> + <child> + <object class="GtkCheckButton" id="checkbox"> + <property name="visible">True</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkBox" id="name_box"> + <property name="visible">True</property> + <property name="margin_start">12</property> + <property name="orientation">vertical</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">20</property> + <property name="xalign">0.0</property> + <property name="yalign">0.5</property> + </object> + </child> + <child> + <object class="GtkLabel" id="description_label"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">20</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">False</property> + <property name="margin_start">12</property> + <property name="width_request">100</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-app-row.c b/src/gs-app-row.c new file mode 100644 index 0000000..721d729 --- /dev/null +++ b/src/gs-app-row.c @@ -0,0 +1,825 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-app-row.h" +#include "gs-star-widget.h" +#include "gs-progress-button.h" +#include "gs-common.h" +#include "gs-folders.h" + +typedef struct +{ + GsApp *app; + GtkWidget *image; + GtkWidget *name_box; + GtkWidget *name_label; + GtkWidget *version_box; + GtkWidget *version_current_label; + GtkWidget *version_arrow_label; + GtkWidget *version_update_label; + GtkWidget *star; + GtkWidget *description_box; + GtkWidget *description_label; + GtkWidget *button_box; + GtkWidget *button; + GtkWidget *spinner; + GtkWidget *label; + GtkWidget *label_warning; + GtkWidget *label_origin; + GtkWidget *label_installed; + GtkWidget *label_app_size; + gboolean colorful; + gboolean show_buttons; + gboolean show_rating; + gboolean show_source; + gboolean show_update; + gboolean show_installed_size; + guint pending_refresh_id; +} GsAppRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsAppRow, gs_app_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + SIGNAL_BUTTON_CLICKED, + SIGNAL_UNREVEALED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +typedef enum { + PROP_APP = 1, + PROP_SHOW_SOURCE, + PROP_SHOW_BUTTONS, + PROP_SHOW_INSTALLED_SIZE, +} GsAppRowProperty; + +static GParamSpec *obj_props[PROP_SHOW_INSTALLED_SIZE + 1] = { NULL, }; + +/** + * gs_app_row_get_description: + * + * Return value: PangoMarkup + **/ +static GString * +gs_app_row_get_description (GsAppRow *app_row) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + const gchar *tmp = NULL; + + /* convert the markdown update description into PangoMarkup */ + if (priv->show_update) { + tmp = gs_app_get_update_details (priv->app); + if (tmp != NULL && tmp[0] != '\0') + return g_string_new (tmp); + } + + /* if missing summary is set, return it without escaping in order to + * correctly show hyperlinks */ + if (gs_app_get_state (priv->app) == AS_APP_STATE_UNAVAILABLE) { + tmp = gs_app_get_summary_missing (priv->app); + if (tmp != NULL && tmp[0] != '\0') + return g_string_new (tmp); + } + + /* try all these things in order */ + if (tmp == NULL || (tmp != NULL && tmp[0] == '\0')) + tmp = gs_app_get_summary (priv->app); + if (tmp == NULL || (tmp != NULL && tmp[0] == '\0')) + tmp = gs_app_get_description (priv->app); + if (tmp == NULL || (tmp != NULL && tmp[0] == '\0')) + tmp = gs_app_get_name (priv->app); + if (tmp == NULL) + return NULL; + return g_string_new (tmp); +} + +static void +gs_app_row_refresh_button (GsAppRow *app_row, gboolean missing_search_result) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + GtkStyleContext *context; + + /* disabled */ + if (!priv->show_buttons) { + gtk_widget_set_visible (priv->button, FALSE); + return; + } + + /* label */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UNAVAILABLE: + gtk_widget_set_visible (priv->button, TRUE); + if (missing_search_result) { + /* TRANSLATORS: this is a button next to the search results that + * allows the application to be easily installed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Visit website")); + } else { + /* TRANSLATORS: this is a button next to the search results that + * allows the application to be easily installed. + * The ellipsis indicates that further steps are required */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Install…")); + } + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (priv->button, TRUE); + /* TRANSLATORS: this is a button next to the search results that + * allows to cancel a queued install of the application */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Cancel")); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + gtk_widget_set_visible (priv->button, TRUE); + /* TRANSLATORS: this is a button next to the search results that + * allows the application to be easily installed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Install")); + break; + case AS_APP_STATE_UPDATABLE_LIVE: + gtk_widget_set_visible (priv->button, TRUE); + if (priv->show_update) { + /* TRANSLATORS: this is a button in the updates panel + * that allows the app to be easily updated live */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Update")); + } else { + /* TRANSLATORS: this is a button next to the search results that + * allows the application to be easily removed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Remove")); + } + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_INSTALLED: + if (!gs_app_has_quirk (priv->app, GS_APP_QUIRK_COMPULSORY)) + gtk_widget_set_visible (priv->button, TRUE); + /* TRANSLATORS: this is a button next to the search results that + * allows the application to be easily removed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Remove")); + break; + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (priv->button, TRUE); + /* TRANSLATORS: this is a button next to the search results that + * shows the status of an application being installed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Installing")); + break; + case AS_APP_STATE_REMOVING: + gtk_widget_set_visible (priv->button, TRUE); + /* TRANSLATORS: this is a button next to the search results that + * shows the status of an application being erased */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Removing")); + break; + default: + break; + } + + /* visible */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UNAVAILABLE: + case AS_APP_STATE_QUEUED_FOR_INSTALL: + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + gtk_widget_set_visible (priv->button, TRUE); + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_INSTALLED: + gtk_widget_set_visible (priv->button, + !gs_app_has_quirk (priv->app, + GS_APP_QUIRK_COMPULSORY)); + break; + default: + gtk_widget_set_visible (priv->button, FALSE); + break; + } + + /* colorful */ + context = gtk_widget_get_style_context (priv->button); + if (!priv->colorful) { + gtk_style_context_remove_class (context, "destructive-action"); + } else { + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_INSTALLED: + gtk_style_context_add_class (context, "destructive-action"); + break; + case AS_APP_STATE_UPDATABLE_LIVE: + if (priv->show_update) + gtk_style_context_remove_class (context, "destructive-action"); + else + gtk_style_context_add_class (context, "destructive-action"); + break; + default: + gtk_style_context_remove_class (context, "destructive-action"); + break; + } + } + + /* always insensitive when in selection mode */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + gtk_widget_set_sensitive (priv->button, FALSE); + break; + default: + gtk_widget_set_sensitive (priv->button, TRUE); + break; + } +} + +static void +gs_app_row_actually_refresh (GsAppRow *app_row) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + GtkStyleContext *context; + GString *str = NULL; + const gchar *tmp; + gboolean missing_search_result; + guint64 size = 0; + + if (priv->app == NULL) + return; + + /* is this a missing search result from the extras page? */ + missing_search_result = (gs_app_get_state (priv->app) == AS_APP_STATE_UNAVAILABLE && + gs_app_get_url (priv->app, AS_URL_KIND_MISSING) != NULL); + + /* do a fill bar for the current progress */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_INSTALLING: + gs_progress_button_set_progress (GS_PROGRESS_BUTTON (priv->button), + gs_app_get_progress (priv->app)); + gs_progress_button_set_show_progress (GS_PROGRESS_BUTTON (priv->button), TRUE); + break; + default: + gs_progress_button_set_show_progress (GS_PROGRESS_BUTTON (priv->button), FALSE); + break; + } + + /* join the description lines */ + str = gs_app_row_get_description (app_row); + if (str != NULL) { + as_utils_string_replace (str, "\n", " "); + gtk_label_set_label (GTK_LABEL (priv->description_label), str->str); + g_string_free (str, TRUE); + } else { + gtk_label_set_text (GTK_LABEL (priv->description_label), NULL); + } + + /* add warning */ + if (gs_app_has_quirk (priv->app, GS_APP_QUIRK_REMOVABLE_HARDWARE)) { + gtk_label_set_text (GTK_LABEL (priv->label_warning), + /* TRANSLATORS: during the update the device + * will restart into a special update-only mode */ + _("Device cannot be used during update.")); + gtk_widget_show (priv->label_warning); + } + + /* where did this app come from */ + if (priv->show_source) { + tmp = gs_app_get_origin_hostname (priv->app); + if (tmp != NULL) { + g_autofree gchar *origin_tmp = NULL; + /* TRANSLATORS: this refers to where the app came from */ + origin_tmp = g_strdup_printf ("%s: %s", _("Source"), tmp); + gtk_label_set_label (GTK_LABEL (priv->label_origin), origin_tmp); + } + gtk_widget_set_visible (priv->label_origin, tmp != NULL); + } else { + gtk_widget_set_visible (priv->label_origin, FALSE); + } + + /* installed tag */ + if (!priv->show_buttons) { + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_INSTALLED: + gtk_widget_set_visible (priv->label_installed, TRUE); + break; + default: + gtk_widget_set_visible (priv->label_installed, FALSE); + break; + } + } else { + gtk_widget_set_visible (priv->label_installed, FALSE); + } + + /* name */ + gtk_label_set_label (GTK_LABEL (priv->name_label), + gs_app_get_name (priv->app)); + + if (priv->show_update) { + const gchar *version_current = NULL; + const gchar *version_update = NULL; + + /* current version */ + tmp = gs_app_get_version_ui (priv->app); + if (tmp != NULL && tmp[0] != '\0') { + version_current = tmp; + gtk_label_set_label (GTK_LABEL (priv->version_current_label), + version_current); + gtk_widget_show (priv->version_current_label); + } else { + gtk_widget_hide (priv->version_current_label); + } + + /* update version */ + tmp = gs_app_get_update_version_ui (priv->app); + if (tmp != NULL && tmp[0] != '\0' && + g_strcmp0 (tmp, version_current) != 0) { + version_update = tmp; + gtk_label_set_label (GTK_LABEL (priv->version_update_label), + version_update); + gtk_widget_show (priv->version_update_label); + } else { + gtk_widget_hide (priv->version_update_label); + } + + /* have both: show arrow */ + if (version_current != NULL && version_update != NULL && + g_strcmp0 (version_current, version_update) != 0) { + gtk_widget_show (priv->version_arrow_label); + } else { + gtk_widget_hide (priv->version_arrow_label); + } + + /* show the box if we have either of the versions */ + if (version_current != NULL || version_update != NULL) + gtk_widget_show (priv->version_box); + else + gtk_widget_hide (priv->version_box); + + gtk_widget_hide (priv->star); + } else { + gtk_widget_hide (priv->version_box); + if (missing_search_result || gs_app_get_rating (priv->app) <= 0 || !priv->show_rating) { + gtk_widget_hide (priv->star); + } else { + gtk_widget_show (priv->star); + gtk_widget_set_sensitive (priv->star, FALSE); + gs_star_widget_set_rating (GS_STAR_WIDGET (priv->star), + gs_app_get_rating (priv->app)); + } + } + + /* pixbuf */ + if (gs_app_get_pixbuf (priv->app) == NULL) { + gtk_image_set_from_icon_name (GTK_IMAGE (priv->image), + "application-x-executable", + GTK_ICON_SIZE_DIALOG); + } else { + gs_image_set_from_pixbuf (GTK_IMAGE (priv->image), + gs_app_get_pixbuf (priv->app)); + } + + context = gtk_widget_get_style_context (priv->image); + if (missing_search_result) + gtk_style_context_add_class (context, "dimmer-label"); + else + gtk_style_context_remove_class (context, "dimmer-label"); + + if (gs_app_get_use_drop_shadow (priv->app)) + gtk_style_context_add_class (context, "icon-dropshadow"); + else + gtk_style_context_remove_class (context, "icon-dropshadow"); + + /* pending label */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (priv->label, TRUE); + gtk_label_set_label (GTK_LABEL (priv->label), _("Pending")); + break; + default: + gtk_widget_set_visible (priv->label, FALSE); + break; + } + + /* spinner */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_REMOVING: + gtk_spinner_start (GTK_SPINNER (priv->spinner)); + gtk_widget_set_visible (priv->spinner, TRUE); + break; + default: + gtk_widget_set_visible (priv->spinner, FALSE); + break; + } + + /* button */ + gs_app_row_refresh_button (app_row, missing_search_result); + + /* hide buttons in the update list, unless the app is live updatable */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (priv->button_box, TRUE); + break; + default: + gtk_widget_set_visible (priv->button_box, !priv->show_update); + break; + } + + /* show the right size */ + if (priv->show_installed_size) { + size = gs_app_get_size_installed (priv->app); + } else if (priv->show_update) { + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_INSTALLING: + size = gs_app_get_size_download (priv->app); + break; + default: + break; + } + } + if (size != GS_APP_SIZE_UNKNOWABLE && size != 0) { + g_autofree gchar *sizestr = NULL; + sizestr = g_format_size (size); + gtk_label_set_label (GTK_LABEL (priv->label_app_size), sizestr); + gtk_widget_show (priv->label_app_size); + } else { + gtk_widget_hide (priv->label_app_size); + } + + /* add warning */ + if (priv->show_update && + gs_app_has_quirk (priv->app, GS_APP_QUIRK_NEW_PERMISSIONS)) { + gtk_label_set_text (GTK_LABEL (priv->label_warning), + _("Requires additional permissions")); + gtk_widget_show (priv->label_warning); + } +} + +static void +child_unrevealed (GObject *revealer, GParamSpec *pspec, gpointer user_data) +{ + GsAppRow *app_row = user_data; + + g_signal_emit (app_row, signals[SIGNAL_UNREVEALED], 0); +} + +void +gs_app_row_unreveal (GsAppRow *app_row) +{ + GtkWidget *child; + GtkWidget *revealer; + + g_return_if_fail (GS_IS_APP_ROW (app_row)); + + child = gtk_bin_get_child (GTK_BIN (app_row)); + gtk_widget_set_sensitive (child, FALSE); + + revealer = gtk_revealer_new (); + gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), TRUE); + gtk_widget_show (revealer); + + g_object_ref (child); + gtk_container_remove (GTK_CONTAINER (app_row), child); + gtk_container_add (GTK_CONTAINER (revealer), child); + g_object_unref (child); + + gtk_container_add (GTK_CONTAINER (app_row), revealer); + g_signal_connect (revealer, "notify::child-revealed", + G_CALLBACK (child_unrevealed), app_row); + gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), FALSE); +} + +GsApp * +gs_app_row_get_app (GsAppRow *app_row) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + g_return_val_if_fail (GS_IS_APP_ROW (app_row), NULL); + return priv->app; +} + +static gboolean +gs_app_row_refresh_idle_cb (gpointer user_data) +{ + GsAppRow *app_row = GS_APP_ROW (user_data); + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + priv->pending_refresh_id = 0; + gs_app_row_actually_refresh (app_row); + return G_SOURCE_REMOVE; +} + +/* Schedule an idle call to gs_app_row_actually_refresh() unless one’s already pending. */ +static void +gs_app_row_schedule_refresh (GsAppRow *app_row) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + if (priv->pending_refresh_id > 0) + return; + priv->pending_refresh_id = g_idle_add (gs_app_row_refresh_idle_cb, app_row); +} + +static void +gs_app_row_notify_props_changed_cb (GsApp *app, + GParamSpec *pspec, + GsAppRow *app_row) +{ + gs_app_row_schedule_refresh (app_row); +} + +static void +gs_app_row_set_app (GsAppRow *app_row, GsApp *app) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->app = g_object_ref (app); + + g_signal_connect_object (priv->app, "notify::state", + G_CALLBACK (gs_app_row_notify_props_changed_cb), + app_row, 0); + g_signal_connect_object (priv->app, "notify::rating", + G_CALLBACK (gs_app_row_notify_props_changed_cb), + app_row, 0); + g_signal_connect_object (priv->app, "notify::progress", + G_CALLBACK (gs_app_row_notify_props_changed_cb), + app_row, 0); + g_signal_connect_object (priv->app, "notify::allow-cancel", + G_CALLBACK (gs_app_row_notify_props_changed_cb), + app_row, 0); + + g_object_notify (G_OBJECT (app_row), "app"); + + gs_app_row_schedule_refresh (app_row); +} + +static void +gs_app_row_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsAppRow *app_row = GS_APP_ROW (object); + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + switch (prop_id) { + case PROP_APP: + g_value_set_object (value, priv->app); + break; + case PROP_SHOW_SOURCE: + g_value_set_boolean (value, priv->show_source); + break; + case PROP_SHOW_BUTTONS: + g_value_set_boolean (value, priv->show_buttons); + break; + case PROP_SHOW_INSTALLED_SIZE: + g_value_set_boolean (value, priv->show_installed_size); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_row_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsAppRow *app_row = GS_APP_ROW (object); + + switch (prop_id) { + case PROP_APP: + gs_app_row_set_app (app_row, g_value_get_object (value)); + break; + case PROP_SHOW_SOURCE: + gs_app_row_set_show_source (app_row, g_value_get_boolean (value)); + break; + case PROP_SHOW_BUTTONS: + gs_app_row_set_show_buttons (app_row, g_value_get_boolean (value)); + break; + case PROP_SHOW_INSTALLED_SIZE: + gs_app_row_set_show_installed_size (app_row, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_row_destroy (GtkWidget *object) +{ + GsAppRow *app_row = GS_APP_ROW (object); + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + if (priv->app) + g_signal_handlers_disconnect_by_func (priv->app, gs_app_row_notify_props_changed_cb, app_row); + + g_clear_object (&priv->app); + if (priv->pending_refresh_id != 0) { + g_source_remove (priv->pending_refresh_id); + priv->pending_refresh_id = 0; + } + + GTK_WIDGET_CLASS (gs_app_row_parent_class)->destroy (object); +} + +static void +gs_app_row_class_init (GsAppRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_app_row_get_property; + object_class->set_property = gs_app_row_set_property; + widget_class->destroy = gs_app_row_destroy; + + /** + * GsAppRow:app: + * + * The #GsApp to show in this row. + * + * Since: 3.38 + */ + obj_props[PROP_APP] = + g_param_spec_object ("app", NULL, NULL, + GS_TYPE_APP, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GsAppRow:show-source: + * + * Show the source of the app in the row. + * + * Since: 3.38 + */ + obj_props[PROP_SHOW_SOURCE] = + g_param_spec_boolean ("show-source", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GsAppRow:show-buttons: + * + * Show buttons (such as Install, Cancel or Update) in the app row. + * + * Since: 3.38 + */ + obj_props[PROP_SHOW_BUTTONS] = + g_param_spec_boolean ("show-buttons", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GsAppRow:show-installed-size: + * + * Show the installed size of the app in the row. + * + * Since: 3.38 + */ + obj_props[PROP_SHOW_INSTALLED_SIZE] = + g_param_spec_boolean ("show-installed-size", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); + + signals [SIGNAL_BUTTON_CLICKED] = + g_signal_new ("button-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsAppRowClass, button_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_UNREVEALED] = + g_signal_new ("unrevealed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsAppRowClass, unrevealed), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, image); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, name_box); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, name_label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, version_box); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, version_current_label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, version_arrow_label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, version_update_label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, star); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, description_box); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, description_label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, button_box); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, button); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, spinner); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, label); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, label_warning); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, label_origin); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, label_installed); + gtk_widget_class_bind_template_child_private (widget_class, GsAppRow, label_app_size); +} + +static void +button_clicked (GtkWidget *widget, GsAppRow *app_row) +{ + g_signal_emit (app_row, signals[SIGNAL_BUTTON_CLICKED], 0); +} + +static void +gs_app_row_init (GsAppRow *app_row) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + gtk_widget_set_has_window (GTK_WIDGET (app_row), FALSE); + gtk_widget_init_template (GTK_WIDGET (app_row)); + + g_signal_connect (priv->button, "clicked", + G_CALLBACK (button_clicked), app_row); +} + +void +gs_app_row_set_size_groups (GsAppRow *app_row, + GtkSizeGroup *image, + GtkSizeGroup *name, + GtkSizeGroup *desc, + GtkSizeGroup *button) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + gtk_size_group_add_widget (image, priv->image); + gtk_size_group_add_widget (name, priv->name_box); + gtk_size_group_add_widget (desc, priv->description_box); + gtk_size_group_add_widget (button, priv->button); +} + +void +gs_app_row_set_colorful (GsAppRow *app_row, gboolean colorful) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->colorful = colorful; + gs_app_row_schedule_refresh (app_row); +} + +void +gs_app_row_set_show_buttons (GsAppRow *app_row, gboolean show_buttons) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->show_buttons = show_buttons; + g_object_notify (G_OBJECT (app_row), "show-buttons"); + gs_app_row_schedule_refresh (app_row); +} + +void +gs_app_row_set_show_rating (GsAppRow *app_row, gboolean show_rating) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->show_rating = show_rating; + gs_app_row_schedule_refresh (app_row); +} + +void +gs_app_row_set_show_source (GsAppRow *app_row, gboolean show_source) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->show_source = show_source; + g_object_notify (G_OBJECT (app_row), "show-source"); + gs_app_row_schedule_refresh (app_row); +} + +void +gs_app_row_set_show_installed_size (GsAppRow *app_row, gboolean show_size) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + priv->show_installed_size = show_size; + g_object_notify (G_OBJECT (app_row), "show-installed-size"); + gs_app_row_schedule_refresh (app_row); +} + +/** + * gs_app_row_set_show_update: + * + * Only really useful for the update panel to call + **/ +void +gs_app_row_set_show_update (GsAppRow *app_row, gboolean show_update) +{ + GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row); + + priv->show_update = show_update; + gs_app_row_schedule_refresh (app_row); +} + +GtkWidget * +gs_app_row_new (GsApp *app) +{ + g_return_val_if_fail (GS_IS_APP (app), NULL); + + return g_object_new (GS_TYPE_APP_ROW, + "app", app, + NULL); +} diff --git a/src/gs-app-row.h b/src/gs-app-row.h new file mode 100644 index 0000000..afe386d --- /dev/null +++ b/src/gs-app-row.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_ROW (gs_app_row_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsAppRow, gs_app_row, GS, APP_ROW, GtkListBoxRow) + +struct _GsAppRowClass +{ + GtkListBoxRowClass parent_class; + void (*button_clicked) (GsAppRow *app_row); + void (*unrevealed) (GsAppRow *app_row); +}; + +GtkWidget *gs_app_row_new (GsApp *app); +void gs_app_row_unreveal (GsAppRow *app_row); +void gs_app_row_set_colorful (GsAppRow *app_row, + gboolean colorful); +void gs_app_row_set_show_buttons (GsAppRow *app_row, + gboolean show_buttons); +void gs_app_row_set_show_rating (GsAppRow *app_row, + gboolean show_rating); +void gs_app_row_set_show_source (GsAppRow *app_row, + gboolean show_source); +void gs_app_row_set_show_update (GsAppRow *app_row, + gboolean show_update); +GsApp *gs_app_row_get_app (GsAppRow *app_row); +void gs_app_row_set_size_groups (GsAppRow *app_row, + GtkSizeGroup *image, + GtkSizeGroup *name, + GtkSizeGroup *desc, + GtkSizeGroup *button); +void gs_app_row_set_show_installed_size (GsAppRow *app_row, + gboolean show_size); + +G_END_DECLS diff --git a/src/gs-app-row.ui b/src/gs-app-row.ui new file mode 100644 index 0000000..109881c --- /dev/null +++ b/src/gs-app-row.ui @@ -0,0 +1,258 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsAppRow" parent="GtkListBoxRow"> + <property name="visible">True</property> + <style> + <class name="list-box-app-row"/> + </style> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="margin_top">16</property> + <property name="margin_bottom">16</property> + <property name="orientation">horizontal</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="pixel_size">64</property> + <property name="margin_start">24</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkBox" id="name_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin-start">12</property> + <property name="spacing">6</property> + <property name="valign">center</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">20</property> + <property name="xalign">0.0</property> + <property name="yalign">0.5</property> + <property name="ellipsize">end</property> + <property name="lines">3</property> + <property name="wrap-mode">word-char</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkBox" id="version_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">4</property> + <child> + <object class="GtkLabel" id="version_current_label"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + <property name="yalign">0.5</property> + <property name="ellipsize">end</property> + </object> + </child> + <child> + <object class="GtkLabel" id="version_arrow_label"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + <property name="yalign">0.5</property> + <property name="ellipsize">end</property> + <property name="label">→</property> + <style> + <class name="version-arrow-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="version_update_label"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + <property name="yalign">0.5</property> + <property name="ellipsize">end</property> + </object> + </child> + </object> + </child> + <child> + <object class="GsStarWidget" id="star"> + <property name="visible">False</property> + <property name="halign">start</property> + <property name="icon-size">12</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="description_box"> + <property name="visible">True</property> + <property name="margin_top">3</property> + <property name="orientation">vertical</property> + <property name="spacing">3</property> + <property name="hexpand">True</property> + <property name="valign">center</property> + <child> + <object class="GtkLabel" id="description_label"> + <property name="visible">True</property> + <property name="valign">start</property> + <property name="vexpand">True</property> + <property name="margin_start">24</property> + <property name="margin_end">24</property> + <property name="wrap">True</property> + <property name="wrap-mode">word-char</property> + <property name="ellipsize">end</property> + <property name="lines">2</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkBox" id="box_tag"> + <property name="visible">True</property> + <property name="spacing">4</property> + <property name="margin_left">24</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="box_desc"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="vexpand">True</property> + <child> + <object class="GtkLabel" id="label_origin"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + <property name="yalign">1.0</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="label_installed"> + <property name="visible">False</property> + <property name="orientation">horizontal</property> + <property name="no_show_all">True</property> + <property name="halign">end</property> + <property name="hexpand">True</property> + <property name="valign">end</property> + <property name="spacing">6</property> + <property name="margin-right">24</property> + <child> + <object class="GtkImage" id="installed-icon"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">4</property> + <property name="pixel-size">16</property> + <property name="icon-name">software-installed-symbolic</property> + <style> + <class name="installed-icon"/> + <class name="app-row-installed-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="installed-label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Installed</property> + <style> + <class name="app-row-installed-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label_warning"> + <property name="visible">False</property> + <property name="label">warning-text</property> + <property name="halign">start</property> + <property name="ellipsize">middle</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="foreground" value="#cccc00000000"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="vertical_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-right">6</property> + <child> + <object class="GtkBox" id="button_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="halign">end</property> + <property name="valign">center</property> + <child> + <object class="GsProgressButton" id="button"> + <property name="visible">False</property> + <property name="width_request">100</property> + <property name="halign">end</property> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + <child> + <object class="GtkSpinner" id="spinner"> + <property name="visible">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="halign">end</property> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="halign">end</property> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label_app_size"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="label">150 MB</property> + <property name="margin-top">6</property> + <style> + <class name="app-row-app-size"/> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-app-tile.c b/src/gs-app-tile.c new file mode 100644 index 0000000..50ee7c9 --- /dev/null +++ b/src/gs-app-tile.c @@ -0,0 +1,113 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-app-tile.h" +#include "gs-common.h" + +typedef struct { + GsApp *app; + guint app_state_changed_idle_id; +} GsAppTilePrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GsAppTile, gs_app_tile, GTK_TYPE_BUTTON) + +GsApp * +gs_app_tile_get_app (GsAppTile *self) +{ + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + g_return_val_if_fail (GS_IS_APP_TILE (self), NULL); + return priv->app; +} + +static gboolean +gs_app_tile_state_changed_idle_cb (gpointer user_data) +{ + GsAppTile *self = GS_APP_TILE (user_data); + GsAppTileClass *klass = GS_APP_TILE_GET_CLASS (self); + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + priv->app_state_changed_idle_id = 0; + klass->refresh (self); + return G_SOURCE_REMOVE; +} + +static void +gs_app_tile_state_changed_cb (GsApp *app, GParamSpec *pspec, GsAppTile *self) +{ + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + g_clear_handle_id (&priv->app_state_changed_idle_id, g_source_remove); + priv->app_state_changed_idle_id = g_idle_add (gs_app_tile_state_changed_idle_cb, self); +} + +void +gs_app_tile_set_app (GsAppTile *self, GsApp *app) +{ + GsAppTileClass *klass = GS_APP_TILE_GET_CLASS (self); + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + + g_return_if_fail (GS_IS_APP_TILE (self)); + g_return_if_fail (!app || GS_IS_APP (app)); + + /* cancel pending refresh */ + g_clear_handle_id (&priv->app_state_changed_idle_id, g_source_remove); + + /* disconnect old app */ + if (priv->app != NULL) + g_signal_handlers_disconnect_by_func (priv->app, gs_app_tile_state_changed_cb, self); + g_set_object (&priv->app, app); + + /* optional refresh */ + if (klass->refresh != NULL && priv->app != NULL) { + g_signal_connect (app, "notify::state", + G_CALLBACK (gs_app_tile_state_changed_cb), self); + g_signal_connect (app, "notify::name", + G_CALLBACK (gs_app_tile_state_changed_cb), self); + g_signal_connect (app, "notify::summary", + G_CALLBACK (gs_app_tile_state_changed_cb), self); + g_signal_connect (app, "notify::key-colors", + G_CALLBACK (gs_app_tile_state_changed_cb), self); + klass->refresh (self); + } +} + +static void +gs_app_tile_finalize (GObject *object) +{ + GsAppTile *self = GS_APP_TILE (object); + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + if (priv->app != NULL) + g_signal_handlers_disconnect_by_func (priv->app, gs_app_tile_state_changed_cb, self); + g_clear_handle_id (&priv->app_state_changed_idle_id, g_source_remove); + g_clear_object (&priv->app); + + G_OBJECT_CLASS (gs_app_tile_parent_class)->finalize (object); +} + +void +gs_app_tile_class_init (GsAppTileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_app_tile_finalize; +} + +void +gs_app_tile_init (GsAppTile *self) +{ + GsAppTilePrivate *priv = gs_app_tile_get_instance_private (self); + priv->app_state_changed_idle_id = 0; +} + +GtkWidget * +gs_app_tile_new (GsApp *app) +{ + GsAppTile *self = g_object_new (GS_TYPE_APP_TILE, NULL); + gs_app_tile_set_app (self, app); + return GTK_WIDGET (self); +} diff --git a/src/gs-app-tile.h b/src/gs-app-tile.h new file mode 100644 index 0000000..f1e28a4 --- /dev/null +++ b/src/gs-app-tile.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_TILE (gs_app_tile_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsAppTile, gs_app_tile, GS, APP_TILE, GtkButton) + +struct _GsAppTileClass +{ + GtkButtonClass parent_class; + void (*refresh) (GsAppTile *self); +}; + +GtkWidget *gs_app_tile_new (GsApp *app); +GsApp *gs_app_tile_get_app (GsAppTile *self); +void gs_app_tile_set_app (GsAppTile *self, + GsApp *app); + +G_END_DECLS diff --git a/src/gs-app-tile.ui b/src/gs-app-tile.ui new file mode 100644 index 0000000..167db8a --- /dev/null +++ b/src/gs-app-tile.ui @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsAppTile" parent="GtkButton"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <!-- This is the minimum (sic!) width of a tile when the GtkFlowBox parent container switches to 3 columns --> + <property name="preferred-width">270</property> + <style> + <class name="view"/> + <class name="tile"/> + </style> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="waiting"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="icon-name">content-loading-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="name">waiting</property> + </packing> + </child> + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <child type="overlay"> + <object class="GtkEventBox" id="eventbox"> + <property name="visible">False</property> + <property name="no_show_all">True</property> + <property name="visible_window">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="margin-top">58</property> + <property name="margin-start">12</property> + <style> + <class name="installed-overlay-box"/> + </style> + <child> + <object class="GtkLabel" id="installed-label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Installed</property> + <property name="margin-start">16</property> + <property name="margin-end">16</property> + <property name="margin-top">4</property> + <property name="margin-bottom">4</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="margin-top">14</property> + <property name="margin-bottom">15</property> + <property name="margin-start">17</property> + <property name="margin-end">17</property> + <property name="row-spacing">3</property> + <property name="column-spacing">12</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="width-request">64</property> + <property name="height-request">64</property> + <style> + <class name="icon-dropshadow"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="name"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + <style> + <class name="app-tile-label"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <property name="yalign">0.0</property> + <property name="lines">2</property> + <property name="vexpand">True</property> + <property name="single-line-mode">True</property> + <style> + <class name="app-tile-label"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">content</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-application.c b/src/gs-application.c new file mode 100644 index 0000000..9c2676e --- /dev/null +++ b/src/gs-application.c @@ -0,0 +1,1150 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-application.h" + +#include <stdlib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> +#include <libsoup/soup.h> + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif + +#ifdef HAVE_PACKAGEKIT +#include "gs-dbus-helper.h" +#endif + +#include "gs-first-run-dialog.h" +#include "gs-shell.h" +#include "gs-update-monitor.h" +#include "gs-shell-search-provider.h" +#include "gs-folders.h" + +#define ENABLE_REPOS_DIALOG_CONF_KEY "enable-repos-dialog" + +struct _GsApplication { + GtkApplication parent; + GCancellable *cancellable; + GtkCssProvider *provider; + GsPluginLoader *plugin_loader; + gint pending_apps; + GsShell *shell; + GsUpdateMonitor *update_monitor; +#ifdef HAVE_PACKAGEKIT + GsDbusHelper *dbus_helper; +#endif + GsShellSearchProvider *search_provider; + GSettings *settings; + GSimpleActionGroup *action_map; + guint shell_loaded_handler_id; +}; + +G_DEFINE_TYPE (GsApplication, gs_application, GTK_TYPE_APPLICATION); + +typedef struct { + GsApplication *app; + GSimpleAction *action; + GVariant *action_param; +} GsActivationHelper; + +static GsActivationHelper * +gs_activation_helper_new (GsApplication *app, + GSimpleAction *action, + GVariant *parameter) +{ + GsActivationHelper *helper = g_slice_new0 (GsActivationHelper); + helper->app = app; + helper->action = G_SIMPLE_ACTION (action); + helper->action_param = parameter; + + return helper; +} + +static void +gs_activation_helper_free (GsActivationHelper *helper) +{ + g_variant_unref (helper->action_param); + g_slice_free (GsActivationHelper, helper); +} + +GsPluginLoader * +gs_application_get_plugin_loader (GsApplication *application) +{ + return application->plugin_loader; +} + +gboolean +gs_application_has_active_window (GsApplication *application) +{ + GList *windows; + + windows = gtk_application_get_windows (GTK_APPLICATION (application)); + for (GList *l = windows; l != NULL; l = l->next) { + if (gtk_window_is_active (GTK_WINDOW (l->data))) + return TRUE; + } + return FALSE; +} + +static void +gs_application_init (GsApplication *application) +{ + const GOptionEntry options[] = { + { "mode", '\0', 0, G_OPTION_ARG_STRING, NULL, + /* TRANSLATORS: this is a command line option */ + _("Start up mode: either ‘updates’, ‘updated’, ‘installed’ or ‘overview’"), _("MODE") }, + { "search", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Search for applications"), _("SEARCH") }, + { "details", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Show application details (using application ID)"), _("ID") }, + { "details-pkg", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Show application details (using package name)"), _("PKGNAME") }, + { "install", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Install the application (using application ID)"), _("ID") }, + { "local-filename", '\0', 0, G_OPTION_ARG_FILENAME, NULL, + _("Open a local package file"), _("FILENAME") }, + { "interaction", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("The kind of interaction expected for this action: either " + "‘none’, ‘notify’, or ‘full’"), NULL }, + { "verbose", '\0', 0, G_OPTION_ARG_NONE, NULL, + _("Show verbose debugging information"), NULL }, + { "autoupdate", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Installs any pending updates in the background"), NULL }, + { "prefs", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Show update preferences"), NULL }, + { "quit", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Quit the running instance"), NULL }, + { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, NULL, + _("Prefer local file sources to AppStream"), NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Show version number"), NULL }, + { NULL } + }; + + g_application_add_main_option_entries (G_APPLICATION (application), options); +} + +static void +gs_application_initialize_plugins (GsApplication *app) +{ + static gboolean initialized = FALSE; + g_auto(GStrv) plugin_blocklist = NULL; + g_auto(GStrv) plugin_allowlist = NULL; + g_autoptr(GError) error = NULL; + const gchar *tmp; + + if (initialized) + return; + + initialized = TRUE; + + /* allow for debugging */ + tmp = g_getenv ("GNOME_SOFTWARE_PLUGINS_BLOCKLIST"); + if (tmp != NULL) + plugin_blocklist = g_strsplit (tmp, ",", -1); + tmp = g_getenv ("GNOME_SOFTWARE_PLUGINS_ALLOWLIST"); + if (tmp != NULL) + plugin_allowlist = g_strsplit (tmp, ",", -1); + + app->plugin_loader = gs_plugin_loader_new (); + if (g_file_test (LOCALPLUGINDIR, G_FILE_TEST_EXISTS)) + gs_plugin_loader_add_location (app->plugin_loader, LOCALPLUGINDIR); + if (!gs_plugin_loader_setup (app->plugin_loader, + plugin_allowlist, + plugin_blocklist, + NULL, + &error)) { + g_warning ("Failed to setup plugins: %s", error->message); + exit (1); + } + + /* show the priority of each plugin */ + gs_plugin_loader_dump_state (app->plugin_loader); + +} + +static gboolean +gs_application_dbus_register (GApplication *application, + GDBusConnection *connection, + const gchar *object_path, + GError **error) +{ + GsApplication *app = GS_APPLICATION (application); + app->search_provider = gs_shell_search_provider_new (); + return gs_shell_search_provider_register (app->search_provider, connection, error); +} + +static void +gs_application_dbus_unregister (GApplication *application, + GDBusConnection *connection, + const gchar *object_path) +{ + GsApplication *app = GS_APPLICATION (application); + + if (app->search_provider != NULL) { + gs_shell_search_provider_unregister (app->search_provider); + g_clear_object (&app->search_provider); + } +} + +static void +gs_application_show_first_run_dialog (GsApplication *app) +{ + GtkWidget *dialog; + + if (g_settings_get_boolean (app->settings, "first-run") == TRUE) { + dialog = gs_first_run_dialog_new (); + gs_shell_modal_dialog_present (app->shell, GTK_DIALOG (dialog)); + g_settings_set_boolean (app->settings, "first-run", FALSE); + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); + } +} + +static void +theme_changed (GtkSettings *settings, GParamSpec *pspec, GsApplication *app) +{ + g_autoptr(GFile) file = NULL; + g_autofree gchar *theme = NULL; + + g_object_get (settings, "gtk-theme-name", &theme, NULL); + if (g_strcmp0 (theme, "HighContrast") == 0) { + file = g_file_new_for_uri ("resource:///org/gnome/Software/gtk-style-hc.css"); + } else { + file = g_file_new_for_uri ("resource:///org/gnome/Software/gtk-style.css"); + } + gtk_css_provider_load_from_file (app->provider, file, NULL); +} + +static void +gs_application_shell_loaded_cb (GsShell *shell, GsApplication *app) +{ + g_signal_handler_disconnect (app->shell, app->shell_loaded_handler_id); + app->shell_loaded_handler_id = 0; +} + +static void +gs_application_initialize_ui (GsApplication *app) +{ + static gboolean initialized = FALSE; + + if (initialized) + return; + + initialized = TRUE; + + /* get CSS */ + app->provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (app->provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_signal_connect (gtk_settings_get_default (), "notify::gtk-theme-name", + G_CALLBACK (theme_changed), app); + theme_changed (gtk_settings_get_default (), NULL, app); + + gs_application_initialize_plugins (app); + + /* setup UI */ + app->shell = gs_shell_new (); + app->cancellable = g_cancellable_new (); + + app->shell_loaded_handler_id = g_signal_connect (app->shell, "loaded", + G_CALLBACK (gs_application_shell_loaded_cb), + app); + + gs_shell_setup (app->shell, app->plugin_loader, app->cancellable); + gtk_application_add_window (GTK_APPLICATION (app), gs_shell_get_window (app->shell)); +} + +static void +gs_application_present_window (GsApplication *app, const gchar *startup_id) +{ + GList *windows; + GtkWindow *window; + + windows = gtk_application_get_windows (GTK_APPLICATION (app)); + if (windows) { + window = windows->data; + + if (startup_id != NULL) + gtk_window_set_startup_id (window, startup_id); + gtk_window_present (window); + } +} + +static void +sources_activated (GSimpleAction *action, + GVariant *parameter, + gpointer app) +{ + gs_shell_show_sources (GS_APPLICATION (app)->shell); +} + +static void +prefs_activated (GSimpleAction *action, GVariant *parameter, gpointer app) +{ + gs_shell_show_prefs (GS_APPLICATION (app)->shell); +} + +static void +about_activated (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + const gchar *authors[] = { + "Richard Hughes", + "Matthias Clasen", + "Kalev Lember", + "Allan Day", + "Ryan Lerch", + "William Jon McCann", + NULL + }; + const gchar *copyright = "Copyright \xc2\xa9 2016-2019 Richard Hughes, Matthias Clasen, Kalev Lember"; + GtkAboutDialog *dialog; + + dialog = GTK_ABOUT_DIALOG (gtk_about_dialog_new ()); + gtk_about_dialog_set_authors (dialog, authors); + gtk_about_dialog_set_copyright (dialog, copyright); + gtk_about_dialog_set_license_type (dialog, GTK_LICENSE_GPL_2_0); + gtk_about_dialog_set_logo_icon_name (dialog, "org.gnome.Software"); + gtk_about_dialog_set_translator_credits (dialog, _("translator-credits")); + gtk_about_dialog_set_version (dialog, VERSION); + gtk_about_dialog_set_program_name (dialog, g_get_application_name ()); + + /* TRANSLATORS: this is the title of the about window */ + gtk_window_set_title (GTK_WINDOW (dialog), _("About Software")); + + /* TRANSLATORS: well, we seem to think so, anyway */ + gtk_about_dialog_set_comments (dialog, _("A nice way to manage the " + "software on your system.")); + + gs_shell_modal_dialog_present (app->shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +static void +cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (app->plugin_loader, res, &error)) { + g_warning ("failed to cancel trigger: %s", error->message); + return; + } +} + +static void +reboot_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) retval = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get result */ + retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error); + if (retval != NULL) + return; + + if (error != NULL) { + g_warning ("Calling org.gnome.SessionManager.Reboot failed: %s", + error->message); + } + + /* cancel trigger */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE_CANCEL, NULL); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + cancel_trigger_failed_cb, + app); +} + +static void +offline_update_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsApplication *app) +{ + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("Failed to trigger offline update: %s", error->message); + return; + } + + /* trigger reboot */ + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, + reboot_failed_cb, + app); +} + +static void +gs_application_reboot_failed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) retval = NULL; + + /* get result */ + retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error); + if (retval != NULL) + return; + if (error != NULL) { + g_warning ("Calling org.gnome.SessionManager.Reboot failed: %s", + error->message); + } +} + +static void +reboot_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + g_autoptr(GDBusConnection) bus = NULL; + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, + gs_application_reboot_failed_cb, + NULL); +} + +static void +shutdown_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + g_application_quit (G_APPLICATION (app)); +} + +static void +reboot_and_install (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + g_autoptr(GsPluginJob) plugin_job = NULL; + gs_application_initialize_plugins (app); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, NULL); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + (GAsyncReadyCallback) offline_update_cb, + app); +} + +static void +quit_activated (GSimpleAction *action, + GVariant *parameter, + gpointer app) +{ + GApplicationFlags flags; + GList *windows; + GtkWidget *window; + + flags = g_application_get_flags (app); + + if (flags & G_APPLICATION_IS_SERVICE) { + windows = gtk_application_get_windows (GTK_APPLICATION (app)); + if (windows) { + window = windows->data; + gtk_widget_hide (window); + } + + return; + } + + g_application_quit (G_APPLICATION (app)); +} + +static void +activate_on_shell_loaded_cb (GsActivationHelper *helper) +{ + GsApplication *app = helper->app; + + g_action_activate (G_ACTION (helper->action), helper->action_param); + + g_signal_handlers_disconnect_by_data (app->shell, helper); + gs_activation_helper_free (helper); +} + +static void +set_mode_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *mode; + + gs_application_present_window (app, NULL); + + gs_shell_reset_state (app->shell); + + mode = g_variant_get_string (parameter, NULL); + if (g_strcmp0 (mode, "updates") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + } else if (g_strcmp0 (mode, "installed") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_INSTALLED); + } else if (g_strcmp0 (mode, "moderate") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_MODERATE); + } else if (g_strcmp0 (mode, "overview") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + } else if (g_strcmp0 (mode, "updated") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_shell_show_installed_updates (app->shell); + } else { + g_warning ("Mode '%s' not recognised", mode); + } +} + +static void +search_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *search; + + gs_application_present_window (app, NULL); + + search = g_variant_get_string (parameter, NULL); + gs_shell_reset_state (app->shell); + gs_shell_show_search (app->shell, search); +} + +static void +_search_launchable_details_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApp *a; + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + list = gs_plugin_loader_job_process_finish (app->plugin_loader, res, &error); + if (list == NULL) { + g_warning ("failed to find application: %s", error->message); + return; + } + if (gs_app_list_length (list) == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + gs_shell_show_notification (app->shell, + /* TRANSLATORS: we tried to show an app that did not exist */ + _("Sorry! There are no details for that application.")); + return; + } + a = gs_app_list_index (list, 0); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +static void +details_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *id; + const gchar *search; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s&s)", &id, &search); + g_debug ("trying to activate %s:%s for details", id, search); + if (search != NULL && search[0] != '\0') { + gs_shell_reset_state (app->shell); + gs_shell_show_search_result (app->shell, id, search); + } else { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (as_utils_unique_id_valid (id)) { + g_autoptr(GsApp) a = gs_plugin_loader_app_create (app->plugin_loader, id); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); + return; + } + + /* find by launchable */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", id, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + _search_launchable_details_cb, + app); + } +} + +static void +details_pkg_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *name; + const gchar *plugin; + g_autoptr (GsApp) a = NULL; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s&s)", &name, &plugin); + a = gs_app_new (NULL); + gs_app_add_source (a, name); + if (strcmp (plugin, "") != 0) + gs_app_set_management_plugin (a, plugin); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +static void +details_url_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *url; + g_autoptr (GsApp) a = NULL; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s)", &url); + + /* this is only used as a wrapper to transport the URL to + * the gs_shell_change_mode() function -- not in the GsAppList */ + a = gs_app_new (NULL); + gs_app_set_metadata (a, "GnomeSoftware::from-url", url); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +static void +install_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *id; + GsShellInteraction interaction; + g_autoptr (GsApp) a = NULL; + + g_variant_get (parameter, "(&su)", &id, &interaction); + if (!as_utils_unique_id_valid (id)) { + g_warning ("Need to use a valid unique-id: %s", id); + return; + } + + if (interaction == GS_SHELL_INTERACTION_FULL) + gs_application_present_window (app, NULL); + + a = gs_plugin_loader_app_create (app->plugin_loader, id); + if (a == NULL) { + g_warning ("Could not create app from unique-id: %s", id); + return; + } + + gs_shell_reset_state (app->shell); + gs_shell_install (app->shell, a, interaction); +} + +static GFile * +_copy_file_to_cache (GFile *file_src, GError **error) +{ + g_autoptr(GFile) file_dest = NULL; + g_autofree gchar *cache_dir = NULL; + g_autofree gchar *cache_fn = NULL; + g_autofree gchar *basename = NULL; + + /* get destination location */ + cache_dir = g_dir_make_tmp ("gnome-software-XXXXXX", error); + if (cache_dir == NULL) + return NULL; + basename = g_file_get_basename (file_src); + cache_fn = g_build_filename (cache_dir, basename, NULL); + + /* copy file to cache */ + file_dest = g_file_new_for_path (cache_fn); + if (!g_file_copy (file_src, file_dest, + G_FILE_COPY_OVERWRITE, + NULL, /* cancellable */ + NULL, NULL, /* progress */ + error)) { + return NULL; + } + return g_steal_pointer (&file_dest); +} + +static void +filename_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *filename; + g_autoptr(GFile) file = NULL; + + g_variant_get (parameter, "(&s)", &filename); + + /* this could go away at any moment, so make a local copy */ + if (g_str_has_prefix (filename, "/tmp") || + g_str_has_prefix (filename, "/var/tmp")) { + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file_src = g_file_new_for_path (filename); + file = _copy_file_to_cache (file_src, &error); + if (file == NULL) { + g_warning ("failed to copy file, falling back to %s: %s", + filename, error->message); + file = g_file_new_for_path (filename); + } + } else { + file = g_file_new_for_path (filename); + } + gs_shell_reset_state (app->shell); + gs_shell_show_local_file (app->shell, file); +} + +static void +launch_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *self = GS_APPLICATION (data); + const gchar *id; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) refine_job = NULL; + g_autoptr(GsPluginJob) launch_job = NULL; + g_autoptr(GError) error = NULL; + + id = g_variant_get_string (parameter, NULL); + app = gs_app_new (id); + refine_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION, + NULL); + if (!gs_plugin_loader_job_action (self->plugin_loader, refine_job, self->cancellable, &error)) { + g_warning ("Failed to refine app: %s", error->message); + return; + } + launch_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "app", app, + NULL); + if (!gs_plugin_loader_job_action (self->plugin_loader, launch_job, self->cancellable, &error)) { + g_warning ("Failed to launch app: %s", error->message); + return; + } +} + +static void +show_offline_updates_error (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + + gs_application_present_window (app, NULL); + + gs_shell_reset_state (app->shell); + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_update_monitor_show_error (app->update_monitor, app->shell); +} + +static void +autoupdate_activated (GSimpleAction *action, GVariant *parameter, gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + gs_shell_reset_state (app->shell); + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_update_monitor_autoupdate (app->update_monitor); +} + +static void +install_resources_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + GdkDisplay *display; + const gchar *mode; + const gchar *startup_id; + g_autofree gchar **resources = NULL; + + g_variant_get (parameter, "(&s^a&s&s)", &mode, &resources, &startup_id); + + display = gdk_display_get_default (); +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (display)) { + if (startup_id != NULL && startup_id[0] != '\0') + gdk_x11_display_set_startup_notification_id (display, + startup_id); + } +#endif +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (display)) { + if (startup_id != NULL && startup_id[0] != '\0') + gdk_wayland_display_set_startup_notification_id (display, + startup_id); + } +#endif + + gs_application_present_window (app, startup_id); + + gs_shell_reset_state (app->shell); + gs_shell_show_extras_search (app->shell, mode, resources); +} + +static GActionEntry actions[] = { + { "about", about_activated, NULL, NULL, NULL }, + { "quit", quit_activated, NULL, NULL, NULL }, + { "reboot-and-install", reboot_and_install, NULL, NULL, NULL }, + { "reboot", reboot_activated, NULL, NULL, NULL }, + { "shutdown", shutdown_activated, NULL, NULL, NULL }, + { "launch", launch_activated, "s", NULL, NULL }, + { "show-offline-update-error", show_offline_updates_error, NULL, NULL, NULL }, + { "autoupdate", autoupdate_activated, NULL, NULL, NULL }, + { "nop", NULL, NULL, NULL } +}; + +static GActionEntry actions_after_loading[] = { + { "sources", sources_activated, NULL, NULL, NULL }, + { "prefs", prefs_activated, NULL, NULL, NULL }, + { "set-mode", set_mode_activated, "s", NULL, NULL }, + { "search", search_activated, "s", NULL, NULL }, + { "details", details_activated, "(ss)", NULL, NULL }, + { "details-pkg", details_pkg_activated, "(ss)", NULL, NULL }, + { "details-url", details_url_activated, "(s)", NULL, NULL }, + { "install", install_activated, "(su)", NULL, NULL }, + { "filename", filename_activated, "(s)", NULL, NULL }, + { "install-resources", install_resources_activated, "(sass)", NULL, NULL }, + { "nop", NULL, NULL, NULL } +}; + +static void +gs_application_update_software_sources_presence (GApplication *self) +{ + GsApplication *app = GS_APPLICATION (self); + GSimpleAction *action; + gboolean enable_sources; + + action = G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (self), + "sources")); + enable_sources = g_settings_get_boolean (app->settings, + ENABLE_REPOS_DIALOG_CONF_KEY); + g_simple_action_set_enabled (action, enable_sources); +} + +static void +gs_application_settings_changed_cb (GApplication *self, + const gchar *key, + gpointer data) +{ + if (g_strcmp0 (key, ENABLE_REPOS_DIALOG_CONF_KEY) == 0) { + gs_application_update_software_sources_presence (self); + } +} + +static void +gs_application_setup_search_provider (GsApplication *app) +{ + gs_application_initialize_plugins (app); + gs_shell_search_provider_setup (app->search_provider, app->plugin_loader); +} + +static void +wrapper_action_activated_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *action_name = g_action_get_name (G_ACTION (action)); + GAction *real_action = g_action_map_lookup_action (G_ACTION_MAP (app->action_map), + action_name); + + if (app->shell_loaded_handler_id != 0) { + GsActivationHelper *helper = gs_activation_helper_new (app, + G_SIMPLE_ACTION (real_action), + g_variant_ref (parameter)); + + g_signal_connect_swapped (app->shell, "loaded", + G_CALLBACK (activate_on_shell_loaded_cb), helper); + return; + } + + g_action_activate (real_action, parameter); +} + +static void +gs_application_add_wrapper_actions (GApplication *application) +{ + GsApplication *app = GS_APPLICATION (application); + GActionMap *map = NULL; + + app->action_map = g_simple_action_group_new (); + map = G_ACTION_MAP (app->action_map); + + /* add the real actions to a different map and add wrapper actions to the + * application instead; the wrapper actions will call the real ones but + * after the "loading state" has finished */ + + g_action_map_add_action_entries (G_ACTION_MAP (map), actions_after_loading, + G_N_ELEMENTS (actions_after_loading), + application); + + for (guint i = 0; i < G_N_ELEMENTS (actions_after_loading); ++i) { + const GActionEntry *entry = &actions_after_loading[i]; + GAction *action = g_action_map_lookup_action (map, entry->name); + g_autoptr (GSimpleAction) simple_action = NULL; + + simple_action = g_simple_action_new (g_action_get_name (action), + g_action_get_parameter_type (action)); + g_signal_connect (simple_action, "activate", + G_CALLBACK (wrapper_action_activated_cb), + application); + g_object_bind_property (simple_action, "enabled", action, + "enabled", G_BINDING_DEFAULT); + g_action_map_add_action (G_ACTION_MAP (application), + G_ACTION (simple_action)); + } +} + +static void +gs_application_startup (GApplication *application) +{ + GSettings *settings; + GsApplication *app = GS_APPLICATION (application); + G_APPLICATION_CLASS (gs_application_parent_class)->startup (application); + + gs_application_add_wrapper_actions (application); + + g_action_map_add_action_entries (G_ACTION_MAP (application), + actions, G_N_ELEMENTS (actions), + application); + + gs_application_setup_search_provider (GS_APPLICATION (application)); + +#ifdef HAVE_PACKAGEKIT + GS_APPLICATION (application)->dbus_helper = gs_dbus_helper_new (); +#endif + settings = g_settings_new ("org.gnome.software"); + GS_APPLICATION (application)->settings = settings; + g_signal_connect_swapped (settings, "changed", + G_CALLBACK (gs_application_settings_changed_cb), + application); + + gs_application_initialize_ui (app); + + GS_APPLICATION (application)->update_monitor = + gs_update_monitor_new (GS_APPLICATION (application)); + gs_folders_convert (); + + gs_application_update_software_sources_presence (application); +} + +static void +gs_application_activate (GApplication *application) +{ + GsApplication *app = GS_APPLICATION (application); + + if (app->shell_loaded_handler_id == 0) + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + + gs_shell_activate (GS_APPLICATION (application)->shell); + + gs_application_show_first_run_dialog (GS_APPLICATION (application)); +} + +static void +gs_application_dispose (GObject *object) +{ + GsApplication *app = GS_APPLICATION (object); + + g_cancellable_cancel (app->cancellable); + g_clear_object (&app->cancellable); + + g_clear_object (&app->plugin_loader); + g_clear_object (&app->shell); + g_clear_object (&app->provider); + g_clear_object (&app->update_monitor); +#ifdef HAVE_PACKAGEKIT + g_clear_object (&app->dbus_helper); +#endif + g_clear_object (&app->settings); + g_clear_object (&app->action_map); + + G_OBJECT_CLASS (gs_application_parent_class)->dispose (object); +} + +static GsShellInteraction +get_page_interaction_from_string (const gchar *interaction) +{ + if (g_strcmp0 (interaction, "notify") == 0) + return GS_SHELL_INTERACTION_NOTIFY; + else if (g_strcmp0 (interaction, "none") == 0) + return GS_SHELL_INTERACTION_NONE; + return GS_SHELL_INTERACTION_FULL; +} + +static int +gs_application_handle_local_options (GApplication *app, GVariantDict *options) +{ + const gchar *id; + const gchar *pkgname; + const gchar *local_filename; + const gchar *mode; + const gchar *search; + gint rc = -1; + g_autoptr(GError) error = NULL; + + if (g_variant_dict_contains (options, "verbose")) + g_setenv ("GS_DEBUG", "1", TRUE); + + /* prefer local sources */ + if (g_variant_dict_contains (options, "prefer-local")) + g_setenv ("GNOME_SOFTWARE_PREFER_LOCAL", "true", TRUE); + + if (g_variant_dict_contains (options, "version")) { + g_print ("gnome-software " VERSION "\n"); + return 0; + } + + if (!g_application_register (app, NULL, &error)) { + g_printerr ("%s\n", error->message); + return 1; + } + + if (g_variant_dict_contains (options, "autoupdate")) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "autoupdate", + NULL); + } + if (g_variant_dict_contains (options, "prefs")) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "prefs", + NULL); + } + if (g_variant_dict_contains (options, "quit")) { + /* The 'quit' command-line option shuts down everything, + * including the backend service */ + g_action_group_activate_action (G_ACTION_GROUP (app), + "shutdown", + NULL); + return 0; + } + + if (g_variant_dict_lookup (options, "mode", "&s", &mode)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "set-mode", + g_variant_new_string (mode)); + rc = 0; + } else if (g_variant_dict_lookup (options, "search", "&s", &search)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "search", + g_variant_new_string (search)); + rc = 0; + } else if (g_variant_dict_lookup (options, "details", "&s", &id)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "details", + g_variant_new ("(ss)", id, "")); + rc = 0; + } else if (g_variant_dict_lookup (options, "details-pkg", "&s", &pkgname)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "details-pkg", + g_variant_new ("(ss)", pkgname, "")); + rc = 0; + } else if (g_variant_dict_lookup (options, "install", "&s", &id)) { + GsShellInteraction interaction = GS_SHELL_INTERACTION_FULL; + const gchar *str_interaction = NULL; + + if (g_variant_dict_lookup (options, "interaction", "&s", + &str_interaction)) + interaction = get_page_interaction_from_string (str_interaction); + + g_action_group_activate_action (G_ACTION_GROUP (app), + "install", + g_variant_new ("(su)", id, + interaction)); + rc = 0; + } else if (g_variant_dict_lookup (options, "local-filename", "^&ay", &local_filename)) { + g_autoptr(GFile) file = NULL; + g_autofree gchar *absolute_filename = NULL; + + file = g_file_new_for_path (local_filename); + absolute_filename = g_file_get_path (file); + g_action_group_activate_action (G_ACTION_GROUP (app), + "filename", + g_variant_new ("(s)", absolute_filename)); + rc = 0; + } + + return rc; +} + +static void +gs_application_open (GApplication *application, + GFile **files, + gint n_files, + const gchar *hint) +{ + GsApplication *app = GS_APPLICATION (application); + gint i; + + for (i = 0; i < n_files; i++) { + g_autofree gchar *str = g_file_get_uri (files[i]); + g_action_group_activate_action (G_ACTION_GROUP (app), + "details-url", + g_variant_new ("(s)", str)); + } +} + +static void +gs_application_class_init (GsApplicationClass *class) +{ + G_OBJECT_CLASS (class)->dispose = gs_application_dispose; + G_APPLICATION_CLASS (class)->startup = gs_application_startup; + G_APPLICATION_CLASS (class)->activate = gs_application_activate; + G_APPLICATION_CLASS (class)->handle_local_options = gs_application_handle_local_options; + G_APPLICATION_CLASS (class)->open = gs_application_open; + G_APPLICATION_CLASS (class)->dbus_register = gs_application_dbus_register; + G_APPLICATION_CLASS (class)->dbus_unregister = gs_application_dbus_unregister; +} + +GsApplication * +gs_application_new (void) +{ + return g_object_new (GS_APPLICATION_TYPE, + "application-id", "org.gnome.Software", + "flags", G_APPLICATION_HANDLES_OPEN, + "inactivity-timeout", 12000, + NULL); +} diff --git a/src/gs-application.h b/src/gs-application.h new file mode 100644 index 0000000..dd56509 --- /dev/null +++ b/src/gs-application.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +#define GS_APPLICATION_TYPE (gs_application_get_type ()) + +G_DECLARE_FINAL_TYPE (GsApplication, gs_application, GS, APPLICATION, GtkApplication) + +GsApplication *gs_application_new (void); +GsPluginLoader *gs_application_get_plugin_loader (GsApplication *application); +gboolean gs_application_has_active_window (GsApplication *application); diff --git a/src/gs-basic-auth-dialog.c b/src/gs-basic-auth-dialog.c new file mode 100644 index 0000000..4c3f0b0 --- /dev/null +++ b/src/gs-basic-auth-dialog.c @@ -0,0 +1,131 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-basic-auth-dialog.h" + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +struct _GsBasicAuthDialog +{ + GtkDialog parent_instance; + + GsBasicAuthCallback callback; + gpointer callback_data; + + /* template widgets */ + GtkButton *login_button; + GtkLabel *description_label; + GtkEntry *user_entry; + GtkEntry *password_entry; +}; + +G_DEFINE_TYPE (GsBasicAuthDialog, gs_basic_auth_dialog, GTK_TYPE_DIALOG) + +static void +cancel_button_clicked_cb (GsBasicAuthDialog *dialog) +{ + /* abort the basic auth request */ + dialog->callback (NULL, NULL, dialog->callback_data); + + gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL); +} + +static void +login_button_clicked_cb (GsBasicAuthDialog *dialog) +{ + const gchar *user; + const gchar *password; + + user = gtk_entry_get_text (dialog->user_entry); + password = gtk_entry_get_text (dialog->password_entry); + + /* submit the user/password to basic auth */ + dialog->callback (user, password, dialog->callback_data); + + gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); +} + +static void +dialog_validate (GsBasicAuthDialog *dialog) +{ + const gchar *user; + const gchar *password; + gboolean valid_user; + gboolean valid_password; + + /* require user */ + user = gtk_entry_get_text (dialog->user_entry); + valid_user = user != NULL && strlen (user) != 0; + + /* require password */ + password = gtk_entry_get_text (dialog->password_entry); + valid_password = password != NULL && strlen (password) != 0; + + gtk_widget_set_sensitive (GTK_WIDGET (dialog->login_button), valid_user && valid_password); +} + +static void +update_description (GsBasicAuthDialog *dialog, const gchar *remote, const gchar *realm) +{ + g_autofree gchar *description = NULL; + + /* TRANSLATORS: This is a description for entering user/password */ + description = g_strdup_printf (_("Login required remote %s (realm %s)"), + remote, realm); + gtk_label_set_text (dialog->description_label, description); +} + +static void +gs_basic_auth_dialog_init (GsBasicAuthDialog *dialog) +{ + gtk_widget_init_template (GTK_WIDGET (dialog)); +} + +static void +gs_basic_auth_dialog_class_init (GsBasicAuthDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-basic-auth-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsBasicAuthDialog, login_button); + gtk_widget_class_bind_template_child (widget_class, GsBasicAuthDialog, description_label); + gtk_widget_class_bind_template_child (widget_class, GsBasicAuthDialog, user_entry); + gtk_widget_class_bind_template_child (widget_class, GsBasicAuthDialog, password_entry); + + gtk_widget_class_bind_template_callback (widget_class, dialog_validate); + gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, login_button_clicked_cb); +} + +GtkWidget * +gs_basic_auth_dialog_new (GtkWindow *parent, + const gchar *remote, + const gchar *realm, + GsBasicAuthCallback callback, + gpointer callback_data) +{ + GsBasicAuthDialog *dialog; + + dialog = g_object_new (GS_TYPE_BASIC_AUTH_DIALOG, + "use-header-bar", TRUE, + "transient-for", parent, + "modal", TRUE, + NULL); + dialog->callback = callback; + dialog->callback_data = callback_data; + + update_description (dialog, remote, realm); + dialog_validate (dialog); + + return GTK_WIDGET (dialog); +} diff --git a/src/gs-basic-auth-dialog.h b/src/gs-basic-auth-dialog.h new file mode 100644 index 0000000..9c07778 --- /dev/null +++ b/src/gs-basic-auth-dialog.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +typedef void (*GsBasicAuthCallback) (const gchar *user, const gchar *password, gpointer callback_data); + +#define GS_TYPE_BASIC_AUTH_DIALOG (gs_basic_auth_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsBasicAuthDialog, gs_basic_auth_dialog, GS, BASIC_AUTH_DIALOG, GtkDialog) + +GtkWidget *gs_basic_auth_dialog_new (GtkWindow *parent, + const gchar *remote, + const gchar *realm, + GsBasicAuthCallback callback, + gpointer callback_data); + +G_END_DECLS diff --git a/src/gs-basic-auth-dialog.ui b/src/gs-basic-auth-dialog.ui new file mode 100644 index 0000000..339e831 --- /dev/null +++ b/src/gs-basic-auth-dialog.ui @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GsBasicAuthDialog" parent="GtkDialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="title" translatable="yes">Login Required</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="show_close_button">False</property> + <child> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="cancel_button_clicked_cb" object="GsBasicAuthDialog" swapped="yes"/> + <style> + <class name="text-button"/> + </style> + </object> + <packing> + <property name="pack_type">start</property> + </packing> + </child> + <child> + <object class="GtkButton" id="login_button"> + <property name="label" translatable="yes">_Login</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="login_button_clicked_cb" object="GsBasicAuthDialog" swapped="yes"/> + <style> + <class name="text-button"/> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="row_spacing">8</property> + <property name="column_spacing">6</property> + <property name="border_width">20</property> + <property name="margin_end">20</property> + <child> + <object class="GtkLabel" id="description_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="margin_bottom">20</property> + <property name="max_width_chars">55</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="user_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_User</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">user_entry</property> + <property name="margin_start">20</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="password_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Password</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">password_entry</property> + <property name="margin_start">20</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="user_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="invisible_char_set">True</property> + <property name="input_purpose">password</property> + <signal name="changed" handler="dialog_validate" object="GsBasicAuthDialog" swapped="yes"/> + <signal name="activate" handler="dialog_validate" object="GsBasicAuthDialog" swapped="yes"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="password_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="visibility">False</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="invisible_char_set">True</property> + <property name="input_purpose">password</property> + <signal name="changed" handler="dialog_validate" object="GsBasicAuthDialog" swapped="yes"/> + <signal name="activate" handler="dialog_validate" object="GsBasicAuthDialog" swapped="yes"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup"> + <widgets> + <widget name="user_label"/> + <widget name="password_label"/> + </widgets> + </object> + <object class="GtkSizeGroup"> + <widgets> + <widget name="user_entry"/> + <widget name="password_entry"/> + </widgets> + </object> + <object class="GtkSizeGroup"> + <property name="mode">horizontal</property> + <widgets> + <widget name="login_button"/> + <widget name="cancel_button"/> + </widgets> + </object> +</interface> diff --git a/src/gs-category-page.c b/src/gs-category-page.c new file mode 100644 index 0000000..0608ac8 --- /dev/null +++ b/src/gs-category-page.c @@ -0,0 +1,594 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-app-list-private.h" +#include "gs-common.h" +#include "gs-summary-tile.h" +#include "gs-popular-tile.h" +#include "gs-category-page.h" +#include "gs-utils.h" + +typedef enum { + SUBCATEGORY_SORT_TYPE_RATING, + SUBCATEGORY_SORT_TYPE_NAME +} SubcategorySortType; + +struct _GsCategoryPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + GsShell *shell; + GsCategory *category; + GsCategory *subcategory; + guint sort_rating_handler_id; + guint sort_name_handler_id; + SubcategorySortType sort_type; + + GtkWidget *category_detail_box; + GtkWidget *scrolledwindow_category; + GtkWidget *subcats_filter_label; + GtkWidget *subcats_filter_button_label; + GtkWidget *subcats_filter_button; + GtkWidget *popover_filter_box; + GtkWidget *subcats_sort_label; + GtkWidget *subcats_sort_button; + GtkWidget *subcats_sort_button_label; + GtkWidget *sort_rating_button; + GtkWidget *sort_name_button; + GtkWidget *featured_grid; + GtkWidget *featured_heading; + GtkWidget *header_filter_box; +}; + +G_DEFINE_TYPE (GsCategoryPage, gs_category_page, GS_TYPE_PAGE) + +static void +gs_category_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (page); + GtkWidget *widget; + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header")); + gtk_widget_show (widget); + gtk_label_set_label (GTK_LABEL (widget), gs_category_get_name (self->category)); +} + +static void +app_tile_clicked (GsAppTile *tile, gpointer data) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (data); + GsApp *app; + + app = gs_app_tile_get_app (tile); + gs_shell_show_app (self->shell, app); +} + +static void +gs_category_page_sort_by_type (GsCategoryPage *self, + SubcategorySortType sort_type) +{ + g_autofree gchar *button_label; + + if (sort_type == SUBCATEGORY_SORT_TYPE_NAME) + g_object_get (self->sort_name_button, "text", &button_label, NULL); + else + g_object_get (self->sort_rating_button, "text", &button_label, NULL); + + gtk_label_set_text (GTK_LABEL (self->subcats_sort_button_label), button_label); + + /* only sort again if the sort type is different */ + if (self->sort_type == sort_type) + return; + + self->sort_type = sort_type; + gtk_flow_box_invalidate_sort (GTK_FLOW_BOX (self->category_detail_box)); +} + +static void +sort_button_clicked (GtkButton *button, gpointer data) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (data); + + if (button == GTK_BUTTON (self->sort_rating_button)) + gs_category_page_sort_by_type (self, SUBCATEGORY_SORT_TYPE_RATING); + else + gs_category_page_sort_by_type (self, SUBCATEGORY_SORT_TYPE_NAME); +} + +static GtkWidget * +make_addon_tile_for_category (GsApp *app, GsCategory *category) +{ + if (g_strcmp0 (gs_category_get_id (category), "fonts") == 0) + return gs_popular_tile_new (app); + + return gs_summary_tile_new (app); +} + +static void +gs_category_page_get_apps_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + guint i; + GsApp *app; + GtkWidget *tile; + GsCategoryPage *self = GS_CATEGORY_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* show an empty space for no results */ + gs_container_remove_all (GTK_CONTAINER (self->category_detail_box)); + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get apps for category apps: %s", error->message); + return; + } + + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + if (g_strcmp0 (gs_category_get_id (self->category), "addons") == 0) { + tile = make_addon_tile_for_category (app, self->subcategory); + } else { + tile = gs_popular_tile_new (app); + } + + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (self->category_detail_box), tile); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + } + + g_signal_handler_unblock (self->sort_rating_button, self->sort_rating_handler_id); + g_signal_handler_unblock (self->sort_name_button, self->sort_name_handler_id); +} + +static gboolean +_max_results_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + return gs_app_get_rating (app1) < gs_app_get_rating (app2); +} + +static gint +gs_category_page_sort_flow_box_sort_func (GtkFlowBoxChild *child1, + GtkFlowBoxChild *child2, + gpointer data) +{ + GsApp *app1 = gs_app_tile_get_app (GS_APP_TILE (gtk_bin_get_child (GTK_BIN (child1)))); + GsApp *app2 = gs_app_tile_get_app (GS_APP_TILE (gtk_bin_get_child (GTK_BIN (child2)))); + SubcategorySortType sort_type; + + if (!GS_IS_APP (app1) || !GS_IS_APP (app2)) + return 0; + + sort_type = GS_CATEGORY_PAGE (data)->sort_type; + + if (sort_type == SUBCATEGORY_SORT_TYPE_RATING) { + gint rating_app1 = gs_app_get_rating (app1); + gint rating_app2 = gs_app_get_rating (app2); + if (rating_app1 > rating_app2) + return -1; + if (rating_app1 < rating_app2) + return 1; + } + + return gs_utils_sort_strcmp (gs_app_get_name (app1), gs_app_get_name (app2)); +} + +static void +gs_category_page_set_featured_placeholders (GsCategoryPage *self) +{ + gs_container_remove_all (GTK_CONTAINER (self->featured_grid)); + for (guint i = 0; i < 3; ++i) { + GtkWidget *tile = gs_summary_tile_new (NULL); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_grid_attach (GTK_GRID (self->featured_grid), tile, i, 0, 1, 1); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + } + gtk_widget_show (self->featured_grid); +} + +static void +gs_category_page_get_featured_apps_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsApp *app; + GtkWidget *tile; + GsCategoryPage *self = GS_CATEGORY_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + gs_container_remove_all (GTK_CONTAINER (self->featured_grid)); + gtk_widget_hide (self->featured_grid); + gtk_widget_hide (self->featured_heading); + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get featured apps for category apps: %s", error->message); + return; + } + if (gs_app_list_length (list) < 3) { + g_debug ("not enough featured apps for category %s; not showing featured apps!", + gs_category_get_id (self->category)); + return; + } + + /* randomize so we show different featured apps every time */ + gs_app_list_randomize (list); + + for (guint i = 0; i < 3; ++i) { + app = gs_app_list_index (list, i); + tile = gs_summary_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_grid_attach (GTK_GRID (self->featured_grid), tile, i, 0, 1, 1); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + } + + gtk_widget_show (self->featured_grid); + gtk_widget_show (self->featured_heading); +} + +static void +gs_category_page_set_featured_apps (GsCategoryPage *self) +{ + GsCategory *featured_subcat = NULL; + GPtrArray *children = gs_category_get_children (self->category); + g_autoptr(GsPluginJob) plugin_job = NULL; + + for (guint i = 0; i < children->len; ++i) { + GsCategory *sub = GS_CATEGORY (g_ptr_array_index (children, i)); + if (g_strcmp0 (gs_category_get_id (sub), "featured") == 0) { + featured_subcat = sub; + break; + } + } + + if (featured_subcat == NULL) + return; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + "interactive", TRUE, + "category", featured_subcat, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, + plugin_job, + self->cancellable, + gs_category_page_get_featured_apps_cb, + self); +} + +static void +gs_category_page_reload (GsPage *page) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (page); + GtkWidget *tile; + guint i, count; + g_autoptr(GsPluginJob) plugin_job = NULL; + + if (self->subcategory == NULL) + return; + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + self->cancellable = g_cancellable_new (); + + g_debug ("search using %s/%s", + gs_category_get_id (self->category), + gs_category_get_id (self->subcategory)); + + /* don't show the sort button on addons that cannot be rated */ + if (g_strcmp0 (gs_category_get_id (self->category), "addons") == 0) { + gtk_widget_set_visible (self->subcats_sort_label, FALSE); + gtk_widget_set_visible (self->subcats_sort_button, FALSE); + + } else { + gtk_widget_set_visible (self->subcats_sort_label, TRUE); + gtk_widget_set_visible (self->subcats_sort_button, TRUE); + } + + g_signal_handler_block (self->sort_rating_button, self->sort_rating_handler_id); + g_signal_handler_block (self->sort_name_button, self->sort_name_handler_id); + + gs_container_remove_all (GTK_CONTAINER (self->category_detail_box)); + + /* just ensure the sort button has the correct label */ + gs_category_page_sort_by_type (self, self->sort_type); + + count = MIN(30, gs_category_get_size (self->subcategory)); + for (i = 0; i < count; i++) { + if (g_strcmp0 (gs_category_get_id (self->category), "addons") == 0) + tile = make_addon_tile_for_category (NULL, self->subcategory); + else + tile = gs_popular_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (self->category_detail_box), tile); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + } + + gs_category_page_set_featured_apps (self); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + "category", self->subcategory, + "filter-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_job_set_sort_func (plugin_job, _max_results_sort_cb); + gs_plugin_loader_job_process_async (self->plugin_loader, + plugin_job, + self->cancellable, + gs_category_page_get_apps_cb, + self); +} + +static void +gs_category_page_populate_filtered (GsCategoryPage *self, GsCategory *subcategory) +{ + g_assert (subcategory != NULL); + g_set_object (&self->subcategory, subcategory); + gs_category_page_reload (GS_PAGE (self)); +} + +static void +filter_button_activated (GtkWidget *button, gpointer data) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (data); + GsCategory *category; + + category = g_object_get_data (G_OBJECT (button), "category"); + + gtk_label_set_text (GTK_LABEL (self->subcats_filter_button_label), + gs_category_get_name (category)); + gs_category_page_populate_filtered (self, category); +} + +static gboolean +gs_category_page_should_use_header_filter (GsCategory *category) +{ + return g_strcmp0 (gs_category_get_id (category), "addons") == 0; +} + +static void +gs_category_page_create_filter (GsCategoryPage *self, + GsCategory *category) +{ + GtkWidget *button = NULL; + GsCategory *s; + guint i; + GPtrArray *children; + GtkWidget *first_subcat = NULL; + gboolean featured_category_found = FALSE; + gboolean use_header_filter = gs_category_page_should_use_header_filter (category); + + gs_container_remove_all (GTK_CONTAINER (self->category_detail_box)); + gs_container_remove_all (GTK_CONTAINER (self->header_filter_box)); + gs_container_remove_all (GTK_CONTAINER (self->popover_filter_box)); + + children = gs_category_get_children (category); + for (i = 0; i < children->len; i++) { + s = GS_CATEGORY (g_ptr_array_index (children, i)); + /* don't include the featured subcategory (those will appear as banners) */ + if (g_strcmp0 (gs_category_get_id (s), "featured") == 0) { + featured_category_found = TRUE; + continue; + } + if (gs_category_get_size (s) < 1) { + g_debug ("not showing %s/%s as no apps", + gs_category_get_id (category), + gs_category_get_id (s)); + continue; + } + + /* create the right button type depending on where it will be used */ + if (use_header_filter) { + if (button == NULL) + button = gtk_radio_button_new (NULL); + else + button = gtk_radio_button_new_from_widget (GTK_RADIO_BUTTON (button)); + g_object_set (button, "xalign", 0.5, "label", gs_category_get_name (s), + "draw-indicator", FALSE, "relief", GTK_RELIEF_NONE, NULL); + gtk_container_add (GTK_CONTAINER (self->header_filter_box), button); + } else { + button = gtk_model_button_new (); + g_object_set (button, "xalign", 0.0, "text", gs_category_get_name (s), NULL); + gtk_container_add (GTK_CONTAINER (self->popover_filter_box), button); + } + + g_object_set_data_full (G_OBJECT (button), "category", g_object_ref (s), g_object_unref); + gtk_widget_show (button); + g_signal_connect (button, "clicked", G_CALLBACK (filter_button_activated), self); + + /* make sure the first subcategory gets selected */ + if (first_subcat == NULL) + first_subcat = button; + } + if (first_subcat != NULL) + filter_button_activated (first_subcat, self); + + /* show only the adequate filter */ + gtk_widget_set_visible (self->subcats_filter_label, !use_header_filter); + gtk_widget_set_visible (self->subcats_filter_button, !use_header_filter); + gtk_widget_set_visible (self->header_filter_box, use_header_filter); + + if (featured_category_found) { + g_autofree gchar *featured_heading = NULL; + + /* set up the placeholders as having the featured category is a good + * indicator that there will be featured apps */ + gs_category_page_set_featured_placeholders (self); + + /* TRANSLATORS: This is a heading on the categories page. %s gets + replaced by the category name, e.g. 'Graphics & Photography' */ + featured_heading = g_strdup_printf (_("Featured %s"), gs_category_get_name (self->category)); + gtk_label_set_label (GTK_LABEL (self->featured_heading), featured_heading); + gtk_widget_show (self->featured_heading); + } else { + gs_container_remove_all (GTK_CONTAINER (self->featured_grid)); + gtk_widget_hide (self->featured_grid); + gtk_widget_hide (self->featured_heading); + } +} + +void +gs_category_page_set_category (GsCategoryPage *self, GsCategory *category) +{ + GtkAdjustment *adj = NULL; + + /* this means we've come from the app-view -> back */ + if (self->category == category) + return; + + /* save this */ + g_clear_object (&self->category); + self->category = g_object_ref (category); + + /* find apps in this group */ + gs_category_page_create_filter (self, category); + + /* scroll the list of apps to the beginning, otherwise it will show + * with the previous scroll value */ + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_category)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); +} + +GsCategory * +gs_category_page_get_category (GsCategoryPage *self) +{ + return self->category; +} + +static void +gs_category_page_init (GsCategoryPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static void +gs_category_page_dispose (GObject *object) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (object); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + if (self->sort_rating_handler_id > 0) { + g_signal_handler_disconnect (self->sort_rating_button, + self->sort_rating_handler_id); + self->sort_rating_handler_id = 0; + } + + if (self->sort_name_handler_id > 0) { + g_signal_handler_disconnect (self->sort_name_button, + self->sort_name_handler_id); + self->sort_name_handler_id = 0; + } + + g_clear_object (&self->builder); + g_clear_object (&self->category); + g_clear_object (&self->subcategory); + g_clear_object (&self->plugin_loader); + + G_OBJECT_CLASS (gs_category_page_parent_class)->dispose (object); +} + +static gboolean +gs_category_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsCategoryPage *self = GS_CATEGORY_PAGE (page); + GtkAdjustment *adj; + + self->plugin_loader = g_object_ref (plugin_loader); + self->builder = g_object_ref (builder); + self->shell = shell; + self->sort_type = SUBCATEGORY_SORT_TYPE_RATING; + gtk_flow_box_set_sort_func (GTK_FLOW_BOX (self->category_detail_box), + gs_category_page_sort_flow_box_sort_func, + self, NULL); + + self->sort_rating_handler_id = g_signal_connect (self->sort_rating_button, + "clicked", + G_CALLBACK (sort_button_clicked), + self); + self->sort_name_handler_id = g_signal_connect (self->sort_name_button, + "clicked", + G_CALLBACK (sort_button_clicked), + self); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_category)); + gtk_container_set_focus_vadjustment (GTK_CONTAINER (self->category_detail_box), adj); + return TRUE; +} + +static void +gs_category_page_class_init (GsCategoryPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_category_page_dispose; + page_class->switch_to = gs_category_page_switch_to; + page_class->reload = gs_category_page_reload; + page_class->setup = gs_category_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-category-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, category_detail_box); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, scrolledwindow_category); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_filter_label); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_filter_button_label); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_filter_button); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, popover_filter_box); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_sort_label); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_sort_button); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_sort_button_label); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, sort_rating_button); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, sort_name_button); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, featured_grid); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, featured_heading); + gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, header_filter_box); +} + +GsCategoryPage * +gs_category_page_new (void) +{ + GsCategoryPage *self; + self = g_object_new (GS_TYPE_CATEGORY_PAGE, NULL); + return self; +} diff --git a/src/gs-category-page.h b/src/gs-category-page.h new file mode 100644 index 0000000..98bf197 --- /dev/null +++ b/src/gs-category-page.h @@ -0,0 +1,25 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_CATEGORY_PAGE (gs_category_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCategoryPage, gs_category_page, GS, CATEGORY_PAGE, GsPage) + +GsCategoryPage *gs_category_page_new (void); +void gs_category_page_set_category (GsCategoryPage *self, + GsCategory *category); +GsCategory *gs_category_page_get_category (GsCategoryPage *self); + +G_END_DECLS diff --git a/src/gs-category-page.ui b/src/gs-category-page.ui new file mode 100644 index 0000000..cbb8284 --- /dev/null +++ b/src/gs-category-page.ui @@ -0,0 +1,256 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <object class="GtkPopover" id="categories_popover"> + <property name="visible">False</property> + <property name="position">bottom</property> + <child> + <object class="GtkBox" id="popover_filter_box"> + <property name="visible">True</property> + <property name="margin">10</property> + <property name="orientation">vertical</property> + </object> + </child> + </object> + <object class="GtkPopover" id="sorting_popover"> + <property name="visible">False</property> + <property name="position">bottom</property> + <child> + <object class="GtkBox" id="sorting_popover_box"> + <property name="visible">True</property> + <property name="margin">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton" id="sort_rating_button"> + <property name="visible">True</property> + <property name="text" translatable="yes" comments="Translators: A label for a button to sort apps by their rating.">Rating</property> + </object> + </child> + <child> + <object class="GtkModelButton" id="sort_name_button"> + <property name="visible">True</property> + <property name="text" translatable="yes" comments="Translators: A label for a button to sort apps alphabetically.">Name</property> + </object> + </child> + </object> + </child> + </object> + <template class="GsCategoryPage" parent="GsPage"> + <child> + <object class="GtkBox" id="box_category"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box_category_results"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">9</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_category"> + <property name="visible">True</property> + <property name="shadow_type">none</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport3"> + <property name="visible">True</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <!-- This is 3*420 plus margins, paddings, CSS borders --> + <property name="preferred-width">1338</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">9</property> + <property name="valign">start</property> + <child> + <object class="GtkBox" id="header_filter_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="homogeneous">True</property> + <style> + <class name="category_page_header_filter_box"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="featured_heading"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="margin_start">24</property> + <property name="margin_top">24</property> + <property name="margin_end">24</property> + <accessibility> + <relation target="featured_grid" type="label-for"/> + </accessibility> + <style> + <class name="index-title-alignment-software"/> + </style> + </object> + </child> + <child> + <object class="GtkGrid" id="featured_grid"> + <property name="visible">False</property> + <property name="column_spacing">14</property> + <property name="margin_start">24</property> + <property name="margin_end">24</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="margin_start">24</property> + <property name="margin_end">24</property> + <property name="margin_top">36</property> + <property name="spacing">9</property> + <child> + <object class="GtkLabel" id="subcats_filter_label"> + <property name="visible">True</property> + <property name="label" translatable="yes" comments="TRANSLATORS: This is a label for the category filter drop down, which all together can read e.g. 'Show Vector Graphics'.">Show</property> + <property name="margin_start">2</property> + <accessibility> + <relation target="subcats_filter_button" type="label-for"/> + </accessibility> + </object> + </child> + <child> + <object class="GtkMenuButton" id="subcats_filter_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">normal</property> + <property name="popover">categories_popover</property> + <property name="margin_end">24</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Subcategories filter menu</property> + </object> + </child> + <child> + <object class="GtkBox" id="grid1"> + <property name="visible">True</property> + <property name="valign">center</property> + <property name="spacing">6</property> + <property name="orientation">horizontal</property> + <child> + <object class="GtkLabel" id="subcats_filter_button_label"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + </object> + </child> + <child> + <object class="GtkArrow" id="arrow1"> + <property name="visible">True</property> + <property name="arrow_type">down</property> + </object> + </child> + </object> + </child> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="subcats_sort_label"> + <property name="visible">True</property> + <property name="label" translatable="yes" comments="TRANSLATORS: This is a label for the category sort drop down, which all together can read e.g. 'Sort Top Rated'.">Sort</property> + <property name="margin_start">2</property> + <accessibility> + <relation target="subcats_sort_button" type="label-for"/> + </accessibility> + </object> + </child> + <child> + <object class="GtkMenuButton" id="subcats_sort_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">normal</property> + <property name="popover">sorting_popover</property> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Subcategories sorting menu</property> + </object> + </child> + <child> + <object class="GtkBox" id="grid2"> + <property name="visible">True</property> + <property name="valign">center</property> + <property name="spacing">6</property> + <property name="orientation">horizontal</property> + <child> + <object class="GtkLabel" id="subcats_sort_button_label"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + </object> + </child> + <child> + <object class="GtkArrow" id="arrow2"> + <property name="visible">True</property> + <property name="arrow_type">down</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + <style> + <class name="text-button"/> + </style> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkFlowBox" id="category_detail_box"> + <property name="margin_start">24</property> + <property name="margin_end">24</property> + <property name="margin_top">14</property> + <property name="margin_bottom">21</property> + <property name="halign">fill</property> + <property name="visible">True</property> + <property name="row_spacing">14</property> + <property name="column_spacing">14</property> + <property name="homogeneous">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="valign">start</property> + <property name="min-children-per-line">2</property> + <property name="selection-mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup"> + <property name="ignore-hidden">False</property> + <property name="mode">horizontal</property> + <widgets> + <widget name="subcats_filter_button_label"/> + <widget name="popover_filter_box"/> + </widgets> + </object> + <object class="GtkSizeGroup"> + <property name="ignore-hidden">False</property> + <property name="mode">horizontal</property> + <widgets> + <widget name="subcats_sort_button_label"/> + <widget name="sorting_popover_box"/> + </widgets> + </object> +</interface> diff --git a/src/gs-category-tile.c b/src/gs-category-tile.c new file mode 100644 index 0000000..b434478 --- /dev/null +++ b/src/gs-category-tile.c @@ -0,0 +1,93 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-category-tile.h" +#include "gs-common.h" + +struct _GsCategoryTile +{ + GtkButton parent_instance; + + GsCategory *cat; + GtkWidget *label; + GtkWidget *image; +}; + +G_DEFINE_TYPE (GsCategoryTile, gs_category_tile, GTK_TYPE_BUTTON) + +GsCategory * +gs_category_tile_get_category (GsCategoryTile *tile) +{ + g_return_val_if_fail (GS_IS_CATEGORY_TILE (tile), NULL); + + return tile->cat; +} + +static void +gs_category_tile_refresh (GsCategoryTile *tile) +{ + /* set labels */ + gtk_label_set_label (GTK_LABEL (tile->label), + gs_category_get_name (tile->cat)); + gtk_image_set_from_icon_name (GTK_IMAGE (tile->image), + gs_category_get_icon (tile->cat), + GTK_ICON_SIZE_MENU); +} + +void +gs_category_tile_set_category (GsCategoryTile *tile, GsCategory *cat) +{ + g_return_if_fail (GS_IS_CATEGORY_TILE (tile)); + g_return_if_fail (GS_IS_CATEGORY (cat)); + + g_set_object (&tile->cat, cat); + gs_category_tile_refresh (tile); +} + +static void +gs_category_tile_destroy (GtkWidget *widget) +{ + GsCategoryTile *tile = GS_CATEGORY_TILE (widget); + + g_clear_object (&tile->cat); + + GTK_WIDGET_CLASS (gs_category_tile_parent_class)->destroy (widget); +} + +static void +gs_category_tile_init (GsCategoryTile *tile) +{ + gtk_widget_set_has_window (GTK_WIDGET (tile), FALSE); + gtk_widget_init_template (GTK_WIDGET (tile)); +} + +static void +gs_category_tile_class_init (GsCategoryTileClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = gs_category_tile_destroy; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-category-tile.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsCategoryTile, label); + gtk_widget_class_bind_template_child (widget_class, GsCategoryTile, image); +} + +GtkWidget * +gs_category_tile_new (GsCategory *cat) +{ + GsCategoryTile *tile; + + tile = g_object_new (GS_TYPE_CATEGORY_TILE, NULL); + gs_category_tile_set_category (tile, cat); + + return GTK_WIDGET (tile); +} diff --git a/src/gs-category-tile.h b/src/gs-category-tile.h new file mode 100644 index 0000000..577587e --- /dev/null +++ b/src/gs-category-tile.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_CATEGORY_TILE (gs_category_tile_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCategoryTile, gs_category_tile, GS, CATEGORY_TILE, GtkButton) + +GtkWidget *gs_category_tile_new (GsCategory *cat); +GsCategory *gs_category_tile_get_category (GsCategoryTile *tile); +void gs_category_tile_set_category (GsCategoryTile *tile, + GsCategory *cat); + +G_END_DECLS diff --git a/src/gs-category-tile.ui b/src/gs-category-tile.ui new file mode 100644 index 0000000..117637d --- /dev/null +++ b/src/gs-category-tile.ui @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsCategoryTile" parent="GtkButton"> + <property name="visible">True</property> + <style> + <class name="view"/> + <class name="tile"/> + <class name="category-tile"/> + </style> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <property name="margin_top">9</property> + <property name="margin_bottom">9</property> + <property name="margin_start">9</property> + <property name="margin_end">9</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="icon_name">folder-music-symbolic</property> + <property name="icon_size">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-common.c b/src/gs-common.c new file mode 100644 index 0000000..1d616e7 --- /dev/null +++ b/src/gs-common.c @@ -0,0 +1,654 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2015 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gdesktopappinfo.h> + +#include "gs-common.h" + +#define SPINNER_DELAY 500 + +static gboolean +fade_in (gpointer data) +{ + GtkWidget *spinner = data; + gdouble opacity; + + opacity = gtk_widget_get_opacity (spinner); + opacity = opacity + 0.1; + gtk_widget_set_opacity (spinner, opacity); + + if (opacity >= 1.0) { + g_object_steal_data (G_OBJECT (spinner), "fade-timeout"); + return G_SOURCE_REMOVE; + } + return G_SOURCE_CONTINUE; +} + +static void +remove_source (gpointer data) +{ + g_source_remove (GPOINTER_TO_UINT (data)); +} + +static gboolean +start_spinning (gpointer data) +{ + GtkWidget *spinner = data; + guint id; + + gtk_widget_set_opacity (spinner, 0); + gtk_spinner_start (GTK_SPINNER (spinner)); + id = g_timeout_add (100, fade_in, spinner); + g_object_set_data_full (G_OBJECT (spinner), "fade-timeout", + GUINT_TO_POINTER (id), remove_source); + + /* don't try to remove this source in the future */ + g_object_steal_data (G_OBJECT (spinner), "start-timeout"); + return G_SOURCE_REMOVE; +} + +void +gs_stop_spinner (GtkSpinner *spinner) +{ + g_object_set_data (G_OBJECT (spinner), "start-timeout", NULL); + gtk_spinner_stop (spinner); +} + +void +gs_start_spinner (GtkSpinner *spinner) +{ + gboolean active; + guint id; + + /* Don't do anything if it's already spinning */ + g_object_get (spinner, "active", &active, NULL); + if (active || g_object_get_data (G_OBJECT (spinner), "start-timeout") != NULL) + return; + + gtk_widget_set_opacity (GTK_WIDGET (spinner), 0); + id = g_timeout_add (SPINNER_DELAY, start_spinning, spinner); + g_object_set_data_full (G_OBJECT (spinner), "start-timeout", + GUINT_TO_POINTER (id), remove_source); +} + +static void +remove_all_cb (GtkWidget *widget, gpointer user_data) +{ + GtkContainer *container = GTK_CONTAINER (user_data); + gtk_container_remove (container, widget); +} + +void +gs_container_remove_all (GtkContainer *container) +{ + gtk_container_foreach (container, remove_all_cb, container); +} + +static void +grab_focus (GtkWidget *widget) +{ + g_signal_handlers_disconnect_by_func (widget, grab_focus, NULL); + gtk_widget_grab_focus (widget); +} + +void +gs_grab_focus_when_mapped (GtkWidget *widget) +{ + if (gtk_widget_get_mapped (widget)) + gtk_widget_grab_focus (widget); + else + g_signal_connect_after (widget, "map", + G_CALLBACK (grab_focus), NULL); +} + +void +gs_app_notify_installed (GsApp *app) +{ + g_autofree gchar *summary = NULL; + const gchar *body = NULL; + g_autoptr(GNotification) n = NULL; + + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_OS_UPDATE: + /* TRANSLATORS: this is the summary of a notification that OS updates + * have been successfully installed */ + summary = g_strdup (_("OS updates are now installed")); + /* TRANSLATORS: this is the body of a notification that OS updates + * have been successfully installed */ + body = _("Recently installed updates are available to review"); + break; + case AS_APP_KIND_DESKTOP: + /* TRANSLATORS: this is the summary of a notification that an application + * has been successfully installed */ + summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app)); + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: an application has been installed, but + * needs a reboot to complete the installation */ + body = _("A restart is required for the changes to take effect."); + } else { + /* TRANSLATORS: this is the body of a notification that an application + * has been successfully installed */ + body = _("Application is ready to be used."); + } + break; + default: + /* TRANSLATORS: this is the summary of a notification that a component + * has been successfully installed */ + summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app)); + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: an application has been installed, but + * needs a reboot to complete the installation */ + body = _("A restart is required for the changes to take effect."); + } + break; + } + n = g_notification_new (summary); + if (body != NULL) + g_notification_set_body (n, body); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: button text */ + g_notification_add_button_with_target (n, _("Restart"), + "app.reboot", NULL); + } else if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP) { + /* TRANSLATORS: this is button that opens the newly installed application */ + g_notification_add_button_with_target (n, _("Launch"), + "app.launch", "s", + gs_app_get_id (app)); + } + g_notification_set_default_action_and_target (n, "app.details", "(ss)", + gs_app_get_unique_id (app), ""); + g_application_send_notification (g_application_get_default (), "installed", n); +} + +typedef enum { + GS_APP_LICENSE_FREE = 0, + GS_APP_LICENSE_NONFREE = 1, + GS_APP_LICENSE_PATENT_CONCERN = 2 +} GsAppLicenseHint; + +GtkResponseType +gs_app_notify_unavailable (GsApp *app, GtkWindow *parent) +{ + GsAppLicenseHint hint = GS_APP_LICENSE_FREE; + GtkResponseType response; + GtkWidget *dialog; + const gchar *license; + gboolean already_enabled = FALSE; /* FIXME */ + guint i; + struct { + const gchar *str; + GsAppLicenseHint hint; + } keywords[] = { + { "NonFree", GS_APP_LICENSE_NONFREE }, + { "PatentConcern", GS_APP_LICENSE_PATENT_CONCERN }, + { "Proprietary", GS_APP_LICENSE_NONFREE }, + { NULL, 0 } + }; + g_autoptr(GSettings) settings = NULL; + g_autoptr(GString) body = NULL; + g_autoptr(GString) title = NULL; + + /* this is very crude */ + license = gs_app_get_license (app); + if (license != NULL) { + for (i = 0; keywords[i].str != NULL; i++) { + if (g_strstr_len (license, -1, keywords[i].str) != NULL) + hint |= keywords[i].hint; + } + } else { + /* use the worst-case assumption */ + hint = GS_APP_LICENSE_NONFREE | GS_APP_LICENSE_PATENT_CONCERN; + } + + /* check if the user has already dismissed */ + settings = g_settings_new ("org.gnome.software"); + if (!g_settings_get_boolean (settings, "prompt-for-nonfree")) + return GTK_RESPONSE_OK; + + title = g_string_new (""); + if (already_enabled) { + g_string_append_printf (title, "<b>%s</b>", + /* TRANSLATORS: window title */ + _("Install Third-Party Software?")); + } else { + g_string_append_printf (title, "<b>%s</b>", + /* TRANSLATORS: window title */ + _("Enable Third-Party Software Repository?")); + } + dialog = gtk_message_dialog_new (parent, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + NULL); + gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), title->str); + + body = g_string_new (""); + if (hint & GS_APP_LICENSE_NONFREE) { + g_string_append_printf (body, + /* TRANSLATORS: the replacements are as follows: + * 1. Application name, e.g. "Firefox" + * 2. Software repository name, e.g. fedora-optional + */ + _("%s is not <a href=\"https://en.wikipedia.org/wiki/Free_and_open-source_software\">" + "free and open source software</a>, " + "and is provided by “%s”."), + gs_app_get_name (app), + gs_app_get_origin (app)); + } else { + g_string_append_printf (body, + /* TRANSLATORS: the replacements are as follows: + * 1. Application name, e.g. "Firefox" + * 2. Software repository name, e.g. fedora-optional */ + _("%s is provided by “%s”."), + gs_app_get_name (app), + gs_app_get_origin (app)); + } + + /* tell the use what needs to be done */ + if (!already_enabled) { + g_string_append (body, " "); + g_string_append (body, + _("This software repository must be " + "enabled to continue installation.")); + } + + /* be aware of patent clauses */ + if (hint & GS_APP_LICENSE_PATENT_CONCERN) { + g_string_append (body, "\n\n"); + if (gs_app_get_kind (app) != AS_APP_KIND_CODEC) { + g_string_append_printf (body, + /* TRANSLATORS: Laws are geographical, urgh... */ + _("It may be illegal to install " + "or use %s in some countries."), + gs_app_get_name (app)); + } else { + g_string_append (body, + /* TRANSLATORS: Laws are geographical, urgh... */ + _("It may be illegal to install or use " + "this codec in some countries.")); + } + } + + gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog), "%s", body->str); + /* TRANSLATORS: this is button text to not ask about non-free content again */ + if (0) gtk_dialog_add_button (GTK_DIALOG (dialog), _("Don’t Warn Again"), GTK_RESPONSE_YES); + if (already_enabled) { + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: button text */ + _("Install"), + GTK_RESPONSE_OK); + } else { + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: button text */ + _("Enable and Install"), + GTK_RESPONSE_OK); + } + response = gtk_dialog_run (GTK_DIALOG (dialog)); + if (response == GTK_RESPONSE_YES) { + response = GTK_RESPONSE_OK; + g_settings_set_boolean (settings, "prompt-for-nonfree", FALSE); + } + gtk_widget_destroy (dialog); + return response; +} + +void +gs_image_set_from_pixbuf_with_scale (GtkImage *image, const GdkPixbuf *pixbuf, gint scale) +{ + cairo_surface_t *surface; + surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL); + if (surface == NULL) + return; + gtk_image_set_from_surface (image, surface); + cairo_surface_destroy (surface); +} + +void +gs_image_set_from_pixbuf (GtkImage *image, const GdkPixbuf *pixbuf) +{ + gint scale; + scale = gdk_pixbuf_get_width (pixbuf) / 64; + gs_image_set_from_pixbuf_with_scale (image, pixbuf, scale); +} + +gboolean +gs_utils_is_current_desktop (const gchar *name) +{ + const gchar *tmp; + g_auto(GStrv) names = NULL; + tmp = g_getenv ("XDG_CURRENT_DESKTOP"); + if (tmp == NULL) + return FALSE; + names = g_strsplit (tmp, ":", -1); + return g_strv_contains ((const gchar * const *) names, name); +} + +static void +gs_utils_widget_css_parsing_error_cb (GtkCssProvider *provider, + GtkCssSection *section, + GError *error, + gpointer user_data) +{ + g_warning ("CSS parse error %u:%u: %s", + gtk_css_section_get_start_line (section), + gtk_css_section_get_start_position (section), + error->message); +} + +/** + * gs_utils_widget_set_css: + * @widget: a widget + * @provider: (inout) (transfer full) (not optional) (nullable): pointer to a + * #GtkCssProvider to use + * @class_name: class name to use, without the leading `.` + * @css: (nullable): CSS to set on the widget, or %NULL to clear custom CSS + * + * Set custom CSS on the given @widget instance. This doesn’t affect any other + * instances of the same widget. The @class_name must be a static string to be + * used as a name for the @css. It doesn’t need to vary with @widget, but + * multiple values of @class_name can be used with the same @widget to control + * several independent snippets of custom CSS. + * + * @provider must be a pointer to a #GtkCssProvider pointer, typically within + * your widget’s private data struct. This function will return a + * #GtkCssProvider in the provided pointer, reusing any old @provider if + * possible. When your widget is destroyed, you must destroy the returned + * @provider. If @css is %NULL, this function will destroy the @provider. + */ +void +gs_utils_widget_set_css (GtkWidget *widget, GtkCssProvider **provider, const gchar *class_name, const gchar *css) +{ + GtkStyleContext *context; + g_autoptr(GString) str = NULL; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (provider != NULL); + g_return_if_fail (provider == NULL || *provider == NULL || GTK_IS_STYLE_PROVIDER (*provider)); + g_return_if_fail (class_name != NULL); + + context = gtk_widget_get_style_context (widget); + + /* remove custom class if NULL */ + if (css == NULL) { + if (*provider != NULL) + gtk_style_context_remove_provider (context, GTK_STYLE_PROVIDER (*provider)); + g_clear_object (provider); + gtk_style_context_remove_class (context, class_name); + return; + } + + str = g_string_sized_new (1024); + g_string_append_printf (str, ".%s {\n", class_name); + g_string_append_printf (str, "%s\n", css); + g_string_append (str, "}"); + + /* create a new provider if needed */ + if (*provider == NULL) { + *provider = gtk_css_provider_new (); + g_signal_connect (*provider, "parsing-error", + G_CALLBACK (gs_utils_widget_css_parsing_error_cb), NULL); + } + + /* set the custom CSS class */ + gtk_style_context_add_class (context, class_name); + + /* set up custom provider and store on the widget */ + gtk_css_provider_load_from_data (*provider, str->str, -1, NULL); + gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (*provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +do_not_expand (GtkWidget *child, gpointer data) +{ + gtk_container_child_set (GTK_CONTAINER (gtk_widget_get_parent (child)), + child, "expand", FALSE, "fill", FALSE, NULL); +} + +static gboolean +unset_focus (GtkWidget *widget, GdkEvent *event, gpointer data) +{ + if (GTK_IS_WINDOW (widget)) + gtk_window_set_focus (GTK_WINDOW (widget), NULL); + return FALSE; +} + +/** + * insert_details_widget: + * @dialog: the message dialog where the widget will be inserted + * @details: the detailed message text to display + * + * Inserts a widget displaying the detailed message into the message dialog. + */ +static void +insert_details_widget (GtkMessageDialog *dialog, const gchar *details) +{ + GtkWidget *message_area, *sw, *label; + GtkWidget *box, *tv; + GtkTextBuffer *buffer; + GList *children; + g_autoptr(GString) msg = NULL; + + g_assert (GTK_IS_MESSAGE_DIALOG (dialog)); + g_assert (details != NULL); + + gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); + + msg = g_string_new (""); + g_string_append_printf (msg, "%s\n\n%s", + /* TRANSLATORS: these are show_detailed_error messages from the + * package manager no mortal is supposed to understand, + * but google might know what they mean */ + _("Detailed errors from the package manager follow:"), + details); + + message_area = gtk_message_dialog_get_message_area (dialog); + g_assert (GTK_IS_BOX (message_area)); + /* make the hbox expand */ + box = gtk_widget_get_parent (message_area); + gtk_container_child_set (GTK_CONTAINER (gtk_widget_get_parent (box)), box, + "expand", TRUE, "fill", TRUE, NULL); + /* make the labels not expand */ + gtk_container_foreach (GTK_CONTAINER (message_area), do_not_expand, NULL); + + /* Find the secondary label and set its width_chars. */ + /* Otherwise the label will tend to expand vertically. */ + children = gtk_container_get_children (GTK_CONTAINER (message_area)); + if (children && children->next && GTK_IS_LABEL (children->next->data)) { + gtk_label_set_width_chars (GTK_LABEL (children->next->data), 40); + } + + label = gtk_label_new (_("Details")); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_visible (label, TRUE); + gtk_container_add (GTK_CONTAINER (message_area), label); + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), + GTK_SHADOW_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (sw), 150); + gtk_widget_set_visible (sw, TRUE); + + tv = gtk_text_view_new (); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); + gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE); + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD); + gtk_style_context_add_class (gtk_widget_get_style_context (tv), + "update-failed-details"); + gtk_text_buffer_set_text (buffer, msg->str, -1); + gtk_widget_set_visible (tv, TRUE); + + gtk_container_add (GTK_CONTAINER (sw), tv); + gtk_widget_set_vexpand (sw, TRUE); + gtk_container_add (GTK_CONTAINER (message_area), sw); + gtk_container_child_set (GTK_CONTAINER (message_area), sw, "pack-type", GTK_PACK_END, NULL); + + g_signal_connect (dialog, "map-event", G_CALLBACK (unset_focus), NULL); +} + +/** + * gs_utils_show_error_dialog: + * @parent: transient parent, or NULL for none + * @title: the title for the dialog + * @msg: the message for the dialog + * @details: (allow-none): the detailed error message, or NULL for none + * + * Shows a message dialog for displaying error messages. + */ +void +gs_utils_show_error_dialog (GtkWindow *parent, + const gchar *title, + const gchar *msg, + const gchar *details) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new_with_markup (parent, + 0, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, + "<big><b>%s</b></big>", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", msg); + if (details != NULL) + insert_details_widget (GTK_MESSAGE_DIALOG (dialog), details); + + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), + dialog); + gtk_widget_show (dialog); +} + +/** + * gs_utils_get_error_value: + * @error: A GError + * + * Gets the machine-readable value stored in the error message. + * The machine readable string is after the first "@", e.g. + * message = "Requires authentication with @aaa" + * + * Returns: a string, or %NULL + */ +const gchar * +gs_utils_get_error_value (const GError *error) +{ + gchar *str; + if (error == NULL) + return NULL; + str = g_strstr_len (error->message, -1, "@"); + if (str == NULL) + return NULL; + return (const gchar *) str + 1; +} + +/** + * gs_utils_build_unique_id_kind: + * @kind: A #AsAppKind + * @id: An application ID + * + * Converts the ID valid into a wildcard unique ID of a specific kind. + * If @id is already a unique ID, then it is returned unchanged. + * + * Returns: (transfer full): a unique ID, or %NULL + */ +gchar * +gs_utils_build_unique_id_kind (AsAppKind kind, const gchar *id) +{ + if (as_utils_unique_id_valid (id)) + return g_strdup (id); + return as_utils_unique_id_build (AS_APP_SCOPE_UNKNOWN, + AS_BUNDLE_KIND_UNKNOWN, + NULL, + kind, + id, + NULL); +} + +/** + * gs_utils_list_has_app_fuzzy: + * @list: A #GsAppList + * @app: A #GsApp + * + * Finds out if any application in the list would match a given application, + * where the match is valid for a matching D-Bus bus name, + * the label in the UI or the same icon. + * + * This function is normally used to work out if the source should be shown + * in a GsAppRow. + * + * Returns: %TRUE if the app is visually the "same" + */ +gboolean +gs_utils_list_has_app_fuzzy (GsAppList *list, GsApp *app) +{ + guint i; + GsApp *tmp; + + for (i = 0; i < gs_app_list_length (list); i++) { + tmp = gs_app_list_index (list, i); + + /* ignore if the same object */ + if (app == tmp) + continue; + + /* ignore with the same source */ + if (g_strcmp0 (gs_app_get_origin_hostname (tmp), + gs_app_get_origin_hostname (app)) == 0) { + continue; + } + + /* same D-Bus ID */ + if (g_strcmp0 (gs_app_get_id (tmp), + gs_app_get_id (app)) == 0) { + return TRUE; + } + + /* same name */ + if (g_strcmp0 (gs_app_get_name (tmp), + gs_app_get_name (app)) == 0) { + return TRUE; + } + } + return FALSE; +} + +void +gs_utils_reboot_notify (GsAppList *list) +{ + g_autoptr(GNotification) n = NULL; + const gchar *title; + const gchar *body; + + /* TRANSLATORS: we've just live-updated some apps */ + title = ngettext ("An update has been installed", + "Updates have been installed", + gs_app_list_length (list)); + + /* TRANSLATORS: the new apps will not be run until we restart */ + body = ngettext ("A restart is required for it to take effect.", + "A restart is required for them to take effect.", + gs_app_list_length (list)); + + n = g_notification_new (title); + g_notification_set_body (n, body); + /* TRANSLATORS: button text */ + g_notification_add_button (n, _("Not Now"), "app.nop"); + /* TRANSLATORS: button text */ + g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + g_notification_set_priority (n, G_NOTIFICATION_PRIORITY_URGENT); + g_application_send_notification (g_application_get_default (), "restart-required", n); +} diff --git a/src/gs-common.h b/src/gs-common.h new file mode 100644 index 0000000..7540aef --- /dev/null +++ b/src/gs-common.h @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gdesktopappinfo.h> +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +void gs_start_spinner (GtkSpinner *spinner); +void gs_stop_spinner (GtkSpinner *spinner); +void gs_container_remove_all (GtkContainer *container); +void gs_grab_focus_when_mapped (GtkWidget *widget); + +void gs_app_notify_installed (GsApp *app); +GtkResponseType + gs_app_notify_unavailable (GsApp *app, + GtkWindow *parent); + +void gs_image_set_from_pixbuf_with_scale (GtkImage *image, + const GdkPixbuf *pixbuf, + gint scale); +void gs_image_set_from_pixbuf (GtkImage *image, + const GdkPixbuf *pixbuf); + +gboolean gs_utils_is_current_desktop (const gchar *name); +void gs_utils_widget_set_css (GtkWidget *widget, + GtkCssProvider **provider, + const gchar *class_name, + const gchar *css); +const gchar *gs_utils_get_error_value (const GError *error); +void gs_utils_show_error_dialog (GtkWindow *parent, + const gchar *title, + const gchar *msg, + const gchar *details); +gchar *gs_utils_build_unique_id_kind (AsAppKind kind, + const gchar *id); +gboolean gs_utils_list_has_app_fuzzy (GsAppList *list, + GsApp *app); +void gs_utils_reboot_notify (GsAppList *list); + +G_END_DECLS diff --git a/src/gs-content-rating.c b/src/gs-content-rating.c new file mode 100644 index 0000000..9746d9b --- /dev/null +++ b/src/gs-content-rating.c @@ -0,0 +1,784 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <glib/gprintf.h> +#include <string.h> +#include "gs-content-rating.h" + +#if !AS_CHECK_VERSION(0, 7, 18) +static const gchar *rating_system_names[] = { + [GS_CONTENT_RATING_SYSTEM_UNKNOWN] = NULL, + [GS_CONTENT_RATING_SYSTEM_INCAA] = "INCAA", + [GS_CONTENT_RATING_SYSTEM_ACB] = "ACB", + [GS_CONTENT_RATING_SYSTEM_DJCTQ] = "DJCTQ", + [GS_CONTENT_RATING_SYSTEM_GSRR] = "GSRR", + [GS_CONTENT_RATING_SYSTEM_PEGI] = "PEGI", + [GS_CONTENT_RATING_SYSTEM_KAVI] = "KAVI", + [GS_CONTENT_RATING_SYSTEM_USK] = "USK", + [GS_CONTENT_RATING_SYSTEM_ESRA] = "ESRA", + [GS_CONTENT_RATING_SYSTEM_CERO] = "CERO", + [GS_CONTENT_RATING_SYSTEM_OFLCNZ] = "OFLCNZ", + [GS_CONTENT_RATING_SYSTEM_RUSSIA] = "RUSSIA", + [GS_CONTENT_RATING_SYSTEM_MDA] = "MDA", + [GS_CONTENT_RATING_SYSTEM_GRAC] = "GRAC", + [GS_CONTENT_RATING_SYSTEM_ESRB] = "ESRB", + [GS_CONTENT_RATING_SYSTEM_IARC] = "IARC", +}; +G_STATIC_ASSERT (G_N_ELEMENTS (rating_system_names) == GS_CONTENT_RATING_SYSTEM_LAST); + +const gchar * +gs_content_rating_system_to_str (GsContentRatingSystem system) +{ + if ((gint) system < GS_CONTENT_RATING_SYSTEM_UNKNOWN || + (gint) system >= GS_CONTENT_RATING_SYSTEM_LAST) + return NULL; + + return rating_system_names[system]; +} + +/* Table of the human-readable descriptions for each #AsContentRatingValue for + * each content rating category. @desc_none must be non-%NULL, but the other + * values may be %NULL if no description is appropriate. In that case, the next + * non-%NULL description for a lower #AsContentRatingValue will be used. */ +static const struct { + const gchar *id; /* (not nullable) */ + const gchar *desc_none; /* (not nullable) */ + const gchar *desc_mild; /* (nullable) */ + const gchar *desc_moderate; /* (nullable) */ + const gchar *desc_intense; /* (nullable) */ +} oars_descriptions[] = { + { + "violence-cartoon", + /* TRANSLATORS: content rating description */ + N_("No cartoon violence"), + /* TRANSLATORS: content rating description */ + N_("Cartoon characters in unsafe situations"), + /* TRANSLATORS: content rating description */ + N_("Cartoon characters in aggressive conflict"), + /* TRANSLATORS: content rating description */ + N_("Graphic violence involving cartoon characters"), + }, + { + "violence-fantasy", + /* TRANSLATORS: content rating description */ + N_("No fantasy violence"), + /* TRANSLATORS: content rating description */ + N_("Characters in unsafe situations easily distinguishable from reality"), + /* TRANSLATORS: content rating description */ + N_("Characters in aggressive conflict easily distinguishable from reality"), + /* TRANSLATORS: content rating description */ + N_("Graphic violence easily distinguishable from reality"), + }, + { + "violence-realistic", + /* TRANSLATORS: content rating description */ + N_("No realistic violence"), + /* TRANSLATORS: content rating description */ + N_("Mildly realistic characters in unsafe situations"), + /* TRANSLATORS: content rating description */ + N_("Depictions of realistic characters in aggressive conflict"), + /* TRANSLATORS: content rating description */ + N_("Graphic violence involving realistic characters"), + }, + { + "violence-bloodshed", + /* TRANSLATORS: content rating description */ + N_("No bloodshed"), + /* TRANSLATORS: content rating description */ + N_("Unrealistic bloodshed"), + /* TRANSLATORS: content rating description */ + N_("Realistic bloodshed"), + /* TRANSLATORS: content rating description */ + N_("Depictions of bloodshed and the mutilation of body parts"), + }, + { + "violence-sexual", + /* TRANSLATORS: content rating description */ + N_("No sexual violence"), + /* TRANSLATORS: content rating description */ + N_("Rape or other violent sexual behavior"), + NULL, + NULL, + }, + { + "drugs-alcohol", + /* TRANSLATORS: content rating description */ + N_("No references to alcohol"), + /* TRANSLATORS: content rating description */ + N_("References to alcoholic beverages"), + /* TRANSLATORS: content rating description */ + N_("Use of alcoholic beverages"), + NULL, + }, + { + "drugs-narcotics", + /* TRANSLATORS: content rating description */ + N_("No references to illicit drugs"), + /* TRANSLATORS: content rating description */ + N_("References to illicit drugs"), + /* TRANSLATORS: content rating description */ + N_("Use of illicit drugs"), + NULL, + }, + { + "drugs-tobacco", + /* TRANSLATORS: content rating description */ + N_("No references to tobacco products"), + /* TRANSLATORS: content rating description */ + N_("References to tobacco products"), + /* TRANSLATORS: content rating description */ + N_("Use of tobacco products"), + NULL, + }, + { + "sex-nudity", + /* TRANSLATORS: content rating description */ + N_("No nudity of any sort"), + /* TRANSLATORS: content rating description */ + N_("Brief artistic nudity"), + /* TRANSLATORS: content rating description */ + N_("Prolonged nudity"), + NULL, + }, + { + "sex-themes", + /* TRANSLATORS: content rating description */ + N_("No references to or depictions of sexual nature"), + /* TRANSLATORS: content rating description */ + N_("Provocative references or depictions"), + /* TRANSLATORS: content rating description */ + N_("Sexual references or depictions"), + /* TRANSLATORS: content rating description */ + N_("Graphic sexual behavior"), + }, + { + "language-profanity", + /* TRANSLATORS: content rating description */ + N_("No profanity of any kind"), + /* TRANSLATORS: content rating description */ + N_("Mild or infrequent use of profanity"), + /* TRANSLATORS: content rating description */ + N_("Moderate use of profanity"), + /* TRANSLATORS: content rating description */ + N_("Strong or frequent use of profanity"), + }, + { + "language-humor", + /* TRANSLATORS: content rating description */ + N_("No inappropriate humor"), + /* TRANSLATORS: content rating description */ + N_("Slapstick humor"), + /* TRANSLATORS: content rating description */ + N_("Vulgar or bathroom humor"), + /* TRANSLATORS: content rating description */ + N_("Mature or sexual humor"), + }, + { + "language-discrimination", + /* TRANSLATORS: content rating description */ + N_("No discriminatory language of any kind"), + /* TRANSLATORS: content rating description */ + N_("Negativity towards a specific group of people"), + /* TRANSLATORS: content rating description */ + N_("Discrimination designed to cause emotional harm"), + /* TRANSLATORS: content rating description */ + N_("Explicit discrimination based on gender, sexuality, race or religion"), + }, + { + "money-advertising", + /* TRANSLATORS: content rating description */ + N_("No advertising of any kind"), + /* TRANSLATORS: content rating description */ + N_("Product placement"), + /* TRANSLATORS: content rating description */ + N_("Explicit references to specific brands or trademarked products"), + /* TRANSLATORS: content rating description */ + N_("Users are encouraged to purchase specific real-world items"), + }, + { + "money-gambling", + /* TRANSLATORS: content rating description */ + N_("No gambling of any kind"), + /* TRANSLATORS: content rating description */ + N_("Gambling on random events using tokens or credits"), + /* TRANSLATORS: content rating description */ + N_("Gambling using “play” money"), + /* TRANSLATORS: content rating description */ + N_("Gambling using real money"), + }, + { + "money-purchasing", + /* TRANSLATORS: content rating description */ + N_("No ability to spend money"), + /* TRANSLATORS: content rating description */ + N_("Users are encouraged to donate real money"), + NULL, + /* TRANSLATORS: content rating description */ + N_("Ability to spend real money in-app"), + }, + { + "social-chat", + /* TRANSLATORS: content rating description */ + N_("No way to chat with other users"), + /* TRANSLATORS: content rating description */ + N_("User-to-user interactions without chat functionality"), + /* TRANSLATORS: content rating description */ + N_("Moderated chat functionality between users"), + /* TRANSLATORS: content rating description */ + N_("Uncontrolled chat functionality between users"), + }, + { + "social-audio", + /* TRANSLATORS: content rating description */ + N_("No way to talk with other users"), + /* TRANSLATORS: content rating description */ + N_("Uncontrolled audio or video chat functionality between users"), + NULL, + NULL, + }, + { + "social-contacts", + /* TRANSLATORS: content rating description */ + N_("No sharing of social network usernames or email addresses"), + /* TRANSLATORS: content rating description */ + N_("Sharing social network usernames or email addresses"), + NULL, + NULL, + }, + { + "social-info", + /* TRANSLATORS: content rating description */ + N_("No sharing of user information with third parties"), + /* TRANSLATORS: content rating description */ + N_("Checking for the latest application version"), + /* TRANSLATORS: content rating description */ + N_("Sharing diagnostic data that does not let others identify the user"), + /* TRANSLATORS: content rating description */ + N_("Sharing information that lets others identify the user"), + }, + { + "social-location", + /* TRANSLATORS: content rating description */ + N_("No sharing of physical location with other users"), + /* TRANSLATORS: content rating description */ + N_("Sharing physical location with other users"), + NULL, + NULL, + }, + + /* v1.1 */ + { + "sex-homosexuality", + /* TRANSLATORS: content rating description */ + N_("No references to homosexuality"), + /* TRANSLATORS: content rating description */ + N_("Indirect references to homosexuality"), + /* TRANSLATORS: content rating description */ + N_("Kissing between people of the same gender"), + /* TRANSLATORS: content rating description */ + N_("Graphic sexual behavior between people of the same gender"), + }, + { + "sex-prostitution", + /* TRANSLATORS: content rating description */ + N_("No references to prostitution"), + /* TRANSLATORS: content rating description */ + N_("Indirect references to prostitution"), + /* TRANSLATORS: content rating description */ + N_("Direct references to prostitution"), + /* TRANSLATORS: content rating description */ + N_("Graphic depictions of the act of prostitution"), + }, + { + "sex-adultery", + /* TRANSLATORS: content rating description */ + N_("No references to adultery"), + /* TRANSLATORS: content rating description */ + N_("Indirect references to adultery"), + /* TRANSLATORS: content rating description */ + N_("Direct references to adultery"), + /* TRANSLATORS: content rating description */ + N_("Graphic depictions of the act of adultery"), + }, + { + "sex-appearance", + /* TRANSLATORS: content rating description */ + N_("No sexualized characters"), + NULL, + /* TRANSLATORS: content rating description */ + N_("Scantily clad human characters"), + /* TRANSLATORS: content rating description */ + N_("Overtly sexualized human characters"), + }, + { + "violence-worship", + /* TRANSLATORS: content rating description */ + N_("No references to desecration"), + /* TRANSLATORS: content rating description */ + N_("Depictions of or references to historical desecration"), + /* TRANSLATORS: content rating description */ + N_("Depictions of modern-day human desecration"), + /* TRANSLATORS: content rating description */ + N_("Graphic depictions of modern-day desecration"), + }, + { + "violence-desecration", + /* TRANSLATORS: content rating description */ + N_("No visible dead human remains"), + /* TRANSLATORS: content rating description */ + N_("Visible dead human remains"), + /* TRANSLATORS: content rating description */ + N_("Dead human remains that are exposed to the elements"), + /* TRANSLATORS: content rating description */ + N_("Graphic depictions of desecration of human bodies"), + }, + { + "violence-slavery", + /* TRANSLATORS: content rating description */ + N_("No references to slavery"), + /* TRANSLATORS: content rating description */ + N_("Depictions of or references to historical slavery"), + /* TRANSLATORS: content rating description */ + N_("Depictions of modern-day slavery"), + /* TRANSLATORS: content rating description */ + N_("Graphic depictions of modern-day slavery"), + }, +}; + +const gchar * +gs_content_rating_key_value_to_str (const gchar *id, AsContentRatingValue value) +{ + gsize i; + + if ((gint) value < AS_CONTENT_RATING_VALUE_NONE || + (gint) value > AS_CONTENT_RATING_VALUE_INTENSE) + return NULL; + + for (i = 0; i < G_N_ELEMENTS (oars_descriptions); i++) { + if (!g_str_equal (oars_descriptions[i].id, id)) + continue; + + /* Return the most-intense non-NULL string. */ + if (oars_descriptions[i].desc_intense != NULL && value >= AS_CONTENT_RATING_VALUE_INTENSE) + return _(oars_descriptions[i].desc_intense); + if (oars_descriptions[i].desc_moderate != NULL && value >= AS_CONTENT_RATING_VALUE_MODERATE) + return _(oars_descriptions[i].desc_moderate); + if (oars_descriptions[i].desc_mild != NULL && value >= AS_CONTENT_RATING_VALUE_MILD) + return _(oars_descriptions[i].desc_mild); + if (oars_descriptions[i].desc_none != NULL && value >= AS_CONTENT_RATING_VALUE_NONE) + return _(oars_descriptions[i].desc_none); + g_assert_not_reached (); + } + + /* This means the requested @id is missing from @oars_descriptions, so + * presumably the OARS spec has been updated but gnome-software hasn’t. */ + g_warn_if_reached (); + + return NULL; +} +#endif /* appstream-glib < 0.7.18 */ + +#if !AS_CHECK_VERSION(0, 7, 15) +/* Equivalent to as_content_rating_get_all_rating_ids() */ +const gchar ** +gs_content_rating_get_all_rating_ids (void) +{ + g_autoptr(GPtrArray) ids = g_ptr_array_new_with_free_func (NULL); + + for (gsize i = 0; i < G_N_ELEMENTS (oars_descriptions); i++) + g_ptr_array_add (ids, (gpointer) oars_descriptions[i].id); + g_ptr_array_add (ids, NULL); + + return (const gchar **) g_ptr_array_free (g_steal_pointer (&ids), FALSE); +} +#endif /* appstream-glib < 0.7.15 */ + +#if !AS_CHECK_VERSION(0, 7, 18) +static char * +get_esrb_string (gchar *source, gchar *translate) +{ + if (g_strcmp0 (source, translate) == 0) + return g_strdup (source); + /* TRANSLATORS: This is the formatting of English and localized name + of the rating e.g. "Adults Only (solo adultos)" */ + return g_strdup_printf (_("%s (%s)"), source, translate); +} + +/* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */ +gchar * +gs_utils_content_rating_age_to_str (GsContentRatingSystem system, guint age) +{ + if (system == GS_CONTENT_RATING_SYSTEM_INCAA) { + if (age >= 18) + return g_strdup ("+18"); + if (age >= 13) + return g_strdup ("+13"); + return g_strdup ("ATP"); + } + if (system == GS_CONTENT_RATING_SYSTEM_ACB) { + if (age >= 18) + return g_strdup ("R18+"); + if (age >= 15) + return g_strdup ("MA15+"); + return g_strdup ("PG"); + } + if (system == GS_CONTENT_RATING_SYSTEM_DJCTQ) { + if (age >= 18) + return g_strdup ("18"); + if (age >= 16) + return g_strdup ("16"); + if (age >= 14) + return g_strdup ("14"); + if (age >= 12) + return g_strdup ("12"); + if (age >= 10) + return g_strdup ("10"); + return g_strdup ("L"); + } + if (system == GS_CONTENT_RATING_SYSTEM_GSRR) { + if (age >= 18) + return g_strdup ("限制"); + if (age >= 15) + return g_strdup ("輔15"); + if (age >= 12) + return g_strdup ("輔12"); + if (age >= 6) + return g_strdup ("保護"); + return g_strdup ("普通"); + } + if (system == GS_CONTENT_RATING_SYSTEM_PEGI) { + if (age >= 18) + return g_strdup ("18"); + if (age >= 16) + return g_strdup ("16"); + if (age >= 12) + return g_strdup ("12"); + if (age >= 7) + return g_strdup ("7"); + if (age >= 3) + return g_strdup ("3"); + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_KAVI) { + if (age >= 18) + return g_strdup ("18+"); + if (age >= 16) + return g_strdup ("16+"); + if (age >= 12) + return g_strdup ("12+"); + if (age >= 7) + return g_strdup ("7+"); + if (age >= 3) + return g_strdup ("3+"); + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_USK) { + if (age >= 18) + return g_strdup ("18"); + if (age >= 16) + return g_strdup ("16"); + if (age >= 12) + return g_strdup ("12"); + if (age >= 6) + return g_strdup ("6"); + return g_strdup ("0"); + } + /* Reference: http://www.esra.org.ir/ */ + if (system == GS_CONTENT_RATING_SYSTEM_ESRA) { + if (age >= 18) + return g_strdup ("+18"); + if (age >= 15) + return g_strdup ("+15"); + if (age >= 12) + return g_strdup ("+12"); + if (age >= 7) + return g_strdup ("+7"); + if (age >= 3) + return g_strdup ("+3"); + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_CERO) { + if (age >= 18) + return g_strdup ("Z"); + if (age >= 17) + return g_strdup ("D"); + if (age >= 15) + return g_strdup ("C"); + if (age >= 12) + return g_strdup ("B"); + return g_strdup ("A"); + } + if (system == GS_CONTENT_RATING_SYSTEM_OFLCNZ) { + if (age >= 18) + return g_strdup ("R18"); + if (age >= 16) + return g_strdup ("R16"); + if (age >= 15) + return g_strdup ("R15"); + if (age >= 13) + return g_strdup ("R13"); + return g_strdup ("G"); + } + if (system == GS_CONTENT_RATING_SYSTEM_RUSSIA) { + if (age >= 18) + return g_strdup ("18+"); + if (age >= 16) + return g_strdup ("16+"); + if (age >= 12) + return g_strdup ("12+"); + if (age >= 6) + return g_strdup ("6+"); + return g_strdup ("0+"); + } + if (system == GS_CONTENT_RATING_SYSTEM_MDA) { + if (age >= 18) + return g_strdup ("M18"); + if (age >= 16) + return g_strdup ("ADV"); + return get_esrb_string ("General", _("General")); + } + if (system == GS_CONTENT_RATING_SYSTEM_GRAC) { + if (age >= 18) + return g_strdup ("18"); + if (age >= 15) + return g_strdup ("15"); + if (age >= 12) + return g_strdup ("12"); + return get_esrb_string ("ALL", _("ALL")); + } + if (system == GS_CONTENT_RATING_SYSTEM_ESRB) { + if (age >= 18) + return get_esrb_string ("Adults Only", _("Adults Only")); + if (age >= 17) + return get_esrb_string ("Mature", _("Mature")); + if (age >= 13) + return get_esrb_string ("Teen", _("Teen")); + if (age >= 10) + return get_esrb_string ("Everyone 10+", _("Everyone 10+")); + if (age >= 6) + return get_esrb_string ("Everyone", _("Everyone")); + + return get_esrb_string ("Early Childhood", _("Early Childhood")); + } + /* IARC = everything else */ + if (age >= 18) + return g_strdup ("18+"); + if (age >= 16) + return g_strdup ("16+"); + if (age >= 12) + return g_strdup ("12+"); + if (age >= 7) + return g_strdup ("7+"); + if (age >= 3) + return g_strdup ("3+"); + return NULL; +} + +/* + * parse_locale: + * @locale: (transfer full): a locale to parse + * @language_out: (out) (optional) (nullable): return location for the parsed + * language, or %NULL to ignore + * @territory_out: (out) (optional) (nullable): return location for the parsed + * territory, or %NULL to ignore + * @codeset_out: (out) (optional) (nullable): return location for the parsed + * codeset, or %NULL to ignore + * @modifier_out: (out) (optional) (nullable): return location for the parsed + * modifier, or %NULL to ignore + * + * Parse @locale as a locale string of the form + * `language[_territory][.codeset][@modifier]` — see `man 3 setlocale` for + * details. + * + * On success, %TRUE will be returned, and the components of the locale will be + * returned in the given addresses, with each component not including any + * separators. Otherwise, %FALSE will be returned and the components will be set + * to %NULL. + * + * @locale is modified, and any returned non-%NULL pointers will point inside + * it. + * + * Returns: %TRUE on success, %FALSE otherwise + */ +static gboolean +parse_locale (gchar *locale /* (transfer full) */, + const gchar **language_out, + const gchar **territory_out, + const gchar **codeset_out, + const gchar **modifier_out) +{ + gchar *separator; + const gchar *language = NULL, *territory = NULL, *codeset = NULL, *modifier = NULL; + + separator = strrchr (locale, '@'); + if (separator != NULL) { + modifier = separator + 1; + *separator = '\0'; + } + + separator = strrchr (locale, '.'); + if (separator != NULL) { + codeset = separator + 1; + *separator = '\0'; + } + + separator = strrchr (locale, '_'); + if (separator != NULL) { + territory = separator + 1; + *separator = '\0'; + } + + language = locale; + + /* Parse failure? */ + if (*language == '\0') { + language = NULL; + territory = NULL; + codeset = NULL; + modifier = NULL; + } + + if (language_out != NULL) + *language_out = language; + if (territory_out != NULL) + *territory_out = territory; + if (codeset_out != NULL) + *codeset_out = codeset; + if (modifier_out != NULL) + *modifier_out = modifier; + + return (language != NULL); +} + +/* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */ +GsContentRatingSystem +gs_utils_content_rating_system_from_locale (const gchar *locale) +{ + g_autofree gchar *locale_copy = g_strdup (locale); + const gchar *territory; + + /* Default to IARC for locales which can’t be parsed. */ + if (!parse_locale (locale_copy, NULL, &territory, NULL, NULL)) + return GS_CONTENT_RATING_SYSTEM_IARC; + + /* Argentina */ + if (g_strcmp0 (territory, "AR") == 0) + return GS_CONTENT_RATING_SYSTEM_INCAA; + + /* Australia */ + if (g_strcmp0 (territory, "AU") == 0) + return GS_CONTENT_RATING_SYSTEM_ACB; + + /* Brazil */ + if (g_strcmp0 (territory, "BR") == 0) + return GS_CONTENT_RATING_SYSTEM_DJCTQ; + + /* Taiwan */ + if (g_strcmp0 (territory, "TW") == 0) + return GS_CONTENT_RATING_SYSTEM_GSRR; + + /* Europe (but not Finland or Germany), India, Israel, + * Pakistan, Quebec, South Africa */ + if ((g_strcmp0 (territory, "GB") == 0) || + g_strcmp0 (territory, "AL") == 0 || + g_strcmp0 (territory, "AD") == 0 || + g_strcmp0 (territory, "AM") == 0 || + g_strcmp0 (territory, "AT") == 0 || + g_strcmp0 (territory, "AZ") == 0 || + g_strcmp0 (territory, "BY") == 0 || + g_strcmp0 (territory, "BE") == 0 || + g_strcmp0 (territory, "BA") == 0 || + g_strcmp0 (territory, "BG") == 0 || + g_strcmp0 (territory, "HR") == 0 || + g_strcmp0 (territory, "CY") == 0 || + g_strcmp0 (territory, "CZ") == 0 || + g_strcmp0 (territory, "DK") == 0 || + g_strcmp0 (territory, "EE") == 0 || + g_strcmp0 (territory, "FR") == 0 || + g_strcmp0 (territory, "GE") == 0 || + g_strcmp0 (territory, "GR") == 0 || + g_strcmp0 (territory, "HU") == 0 || + g_strcmp0 (territory, "IS") == 0 || + g_strcmp0 (territory, "IT") == 0 || + g_strcmp0 (territory, "LZ") == 0 || + g_strcmp0 (territory, "XK") == 0 || + g_strcmp0 (territory, "LV") == 0 || + g_strcmp0 (territory, "FL") == 0 || + g_strcmp0 (territory, "LU") == 0 || + g_strcmp0 (territory, "LT") == 0 || + g_strcmp0 (territory, "MK") == 0 || + g_strcmp0 (territory, "MT") == 0 || + g_strcmp0 (territory, "MD") == 0 || + g_strcmp0 (territory, "MC") == 0 || + g_strcmp0 (territory, "ME") == 0 || + g_strcmp0 (territory, "NL") == 0 || + g_strcmp0 (territory, "NO") == 0 || + g_strcmp0 (territory, "PL") == 0 || + g_strcmp0 (territory, "PT") == 0 || + g_strcmp0 (territory, "RO") == 0 || + g_strcmp0 (territory, "SM") == 0 || + g_strcmp0 (territory, "RS") == 0 || + g_strcmp0 (territory, "SK") == 0 || + g_strcmp0 (territory, "SI") == 0 || + g_strcmp0 (territory, "ES") == 0 || + g_strcmp0 (territory, "SE") == 0 || + g_strcmp0 (territory, "CH") == 0 || + g_strcmp0 (territory, "TR") == 0 || + g_strcmp0 (territory, "UA") == 0 || + g_strcmp0 (territory, "VA") == 0 || + g_strcmp0 (territory, "IN") == 0 || + g_strcmp0 (territory, "IL") == 0 || + g_strcmp0 (territory, "PK") == 0 || + g_strcmp0 (territory, "ZA") == 0) + return GS_CONTENT_RATING_SYSTEM_PEGI; + + /* Finland */ + if (g_strcmp0 (territory, "FI") == 0) + return GS_CONTENT_RATING_SYSTEM_KAVI; + + /* Germany */ + if (g_strcmp0 (territory, "DE") == 0) + return GS_CONTENT_RATING_SYSTEM_USK; + + /* Iran */ + if (g_strcmp0 (territory, "IR") == 0) + return GS_CONTENT_RATING_SYSTEM_ESRA; + + /* Japan */ + if (g_strcmp0 (territory, "JP") == 0) + return GS_CONTENT_RATING_SYSTEM_CERO; + + /* New Zealand */ + if (g_strcmp0 (territory, "NZ") == 0) + return GS_CONTENT_RATING_SYSTEM_OFLCNZ; + + /* Russia: Content rating law */ + if (g_strcmp0 (territory, "RU") == 0) + return GS_CONTENT_RATING_SYSTEM_RUSSIA; + + /* Singapore */ + if (g_strcmp0 (territory, "SQ") == 0) + return GS_CONTENT_RATING_SYSTEM_MDA; + + /* South Korea */ + if (g_strcmp0 (territory, "KR") == 0) + return GS_CONTENT_RATING_SYSTEM_GRAC; + + /* USA, Canada, Mexico */ + if ((g_strcmp0 (territory, "US") == 0) || + g_strcmp0 (territory, "CA") == 0 || + g_strcmp0 (territory, "MX") == 0) + return GS_CONTENT_RATING_SYSTEM_ESRB; + + /* everything else is IARC */ + return GS_CONTENT_RATING_SYSTEM_IARC; +} +#endif /* appstream-glib < 0.7.18 */ diff --git a/src/gs-content-rating.h b/src/gs-content-rating.h new file mode 100644 index 0000000..0e31784 --- /dev/null +++ b/src/gs-content-rating.h @@ -0,0 +1,76 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +G_BEGIN_DECLS + +#include <appstream-glib.h> +#include <glib-object.h> + +#if AS_CHECK_VERSION(0, 7, 18) +#define GS_CONTENT_RATING_SYSTEM_UNKNOWN AS_CONTENT_RATING_SYSTEM_UNKNOWN +#define GS_CONTENT_RATING_SYSTEM_INCAA AS_CONTENT_RATING_SYSTEM_INCAA +#define GS_CONTENT_RATING_SYSTEM_ACB AS_CONTENT_RATING_SYSTEM_ACB +#define GS_CONTENT_RATING_SYSTEM_DJCTQ AS_CONTENT_RATING_SYSTEM_DJCTQ +#define GS_CONTENT_RATING_SYSTEM_GSRR AS_CONTENT_RATING_SYSTEM_GSRR +#define GS_CONTENT_RATING_SYSTEM_PEGI AS_CONTENT_RATING_SYSTEM_PEGI +#define GS_CONTENT_RATING_SYSTEM_KAVI AS_CONTENT_RATING_SYSTEM_KAVI +#define GS_CONTENT_RATING_SYSTEM_USK AS_CONTENT_RATING_SYSTEM_USK +#define GS_CONTENT_RATING_SYSTEM_ESRA AS_CONTENT_RATING_SYSTEM_ESRA +#define GS_CONTENT_RATING_SYSTEM_CERO AS_CONTENT_RATING_SYSTEM_CERO +#define GS_CONTENT_RATING_SYSTEM_OFLCNZ AS_CONTENT_RATING_SYSTEM_OFLCNZ +#define GS_CONTENT_RATING_SYSTEM_RUSSIA AS_CONTENT_RATING_SYSTEM_RUSSIA +#define GS_CONTENT_RATING_SYSTEM_MDA AS_CONTENT_RATING_SYSTEM_MDA +#define GS_CONTENT_RATING_SYSTEM_GRAC AS_CONTENT_RATING_SYSTEM_GRAC +#define GS_CONTENT_RATING_SYSTEM_ESRB AS_CONTENT_RATING_SYSTEM_ESRB +#define GS_CONTENT_RATING_SYSTEM_IARC AS_CONTENT_RATING_SYSTEM_IARC +#define GS_CONTENT_RATING_SYSTEM_LAST AS_CONTENT_RATING_SYSTEM_LAST +#define GsContentRatingSystem AsContentRatingSystem + +#define gs_utils_content_rating_age_to_str as_content_rating_system_format_age +#define gs_utils_content_rating_system_from_locale as_content_rating_system_from_locale +#define gs_content_rating_key_value_to_str as_content_rating_attribute_get_description +#define gs_content_rating_system_to_str as_content_rating_system_to_string +#else +typedef enum { + GS_CONTENT_RATING_SYSTEM_UNKNOWN, + GS_CONTENT_RATING_SYSTEM_INCAA, + GS_CONTENT_RATING_SYSTEM_ACB, + GS_CONTENT_RATING_SYSTEM_DJCTQ, + GS_CONTENT_RATING_SYSTEM_GSRR, + GS_CONTENT_RATING_SYSTEM_PEGI, + GS_CONTENT_RATING_SYSTEM_KAVI, + GS_CONTENT_RATING_SYSTEM_USK, + GS_CONTENT_RATING_SYSTEM_ESRA, + GS_CONTENT_RATING_SYSTEM_CERO, + GS_CONTENT_RATING_SYSTEM_OFLCNZ, + GS_CONTENT_RATING_SYSTEM_RUSSIA, + GS_CONTENT_RATING_SYSTEM_MDA, + GS_CONTENT_RATING_SYSTEM_GRAC, + GS_CONTENT_RATING_SYSTEM_ESRB, + GS_CONTENT_RATING_SYSTEM_IARC, + /*< private >*/ + GS_CONTENT_RATING_SYSTEM_LAST +} GsContentRatingSystem; + +gchar *gs_utils_content_rating_age_to_str (GsContentRatingSystem system, + guint age); +GsContentRatingSystem gs_utils_content_rating_system_from_locale (const gchar *locale); +const gchar *gs_content_rating_key_value_to_str (const gchar *id, + AsContentRatingValue value); +const gchar *gs_content_rating_system_to_str (GsContentRatingSystem system); +#endif + +#if AS_CHECK_VERSION(0, 7, 15) +#define gs_content_rating_get_all_rating_ids as_content_rating_get_all_rating_ids +#else +const gchar **gs_content_rating_get_all_rating_ids (void); +#endif + +G_END_DECLS diff --git a/src/gs-css.c b/src/gs-css.c new file mode 100644 index 0000000..923ac1a --- /dev/null +++ b/src/gs-css.c @@ -0,0 +1,305 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-css + * @title: GsCss + * @stability: Unstable + * @short_description: Parse, validate and rewrite CSS resources + */ + +#include "config.h" + +#include <gtk/gtk.h> +#include <appstream-glib.h> + +#include "gs-css.h" + +struct _GsCss +{ + GObject parent_instance; + GHashTable *ids; + GsCssRewriteFunc rewrite_func; + gpointer rewrite_func_data; +}; + +G_DEFINE_TYPE (GsCss, gs_css, G_TYPE_OBJECT) + +static void +_cleanup_string (GString *str) +{ + /* remove leading newlines */ + while (g_str_has_prefix (str->str, "\n") || g_str_has_prefix (str->str, " ")) + g_string_erase (str, 0, 1); + + /* remove trailing newlines */ + while (g_str_has_suffix (str->str, "\n") || g_str_has_suffix (str->str, " ")) + g_string_truncate (str, str->len - 1); +} + +/** + * gs_css_parse: + * @self: a #GsCss + * @markup: come CSS, or %NULL + * @error: a #GError or %NULL + * + * Parses the CSS markup and does some basic validation checks on the input. + * + * Returns: %TRUE for success + */ +gboolean +gs_css_parse (GsCss *self, const gchar *markup, GError **error) +{ + g_auto(GStrv) parts = NULL; + g_autoptr(GString) markup_str = NULL; + + /* no data */ + if (markup == NULL || markup[0] == '\0') + return TRUE; + + /* old style, no IDs */ + markup_str = g_string_new (markup); + as_utils_string_replace (markup_str, "@datadir@", DATADIR); + if (!g_str_has_prefix (markup_str->str, "#")) { + g_hash_table_insert (self->ids, + g_strdup ("tile"), + g_string_free (g_steal_pointer (&markup_str), FALSE)); + return TRUE; + } + + /* split up CSS into ID chunks, e.g. + * + * #tile {border-radius: 0;} + * #name {color: white;} + */ + parts = g_strsplit (markup_str->str + 1, "\n#", -1); + for (guint i = 0; parts[i] != NULL; i++) { + g_autoptr(GString) current_css = NULL; + g_autoptr(GString) current_key = NULL; + for (guint j = 1; parts[i][j] != '\0'; j++) { + const gchar ch = parts[i][j]; + if (ch == '{') { + if (current_key != NULL || current_css != NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "invalid '{'"); + return FALSE; + } + current_key = g_string_new_len (parts[i], j); + current_css = g_string_new (NULL); + _cleanup_string (current_key); + + /* already added */ + if (g_hash_table_lookup (self->ids, current_key->str) != NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "duplicate ID '%s'", + current_key->str); + return FALSE; + } + continue; + } + if (ch == '}') { + if (current_key == NULL || current_css == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "invalid '}'"); + return FALSE; + } + _cleanup_string (current_css); + g_hash_table_insert (self->ids, + g_string_free (current_key, FALSE), + g_string_free (current_css, FALSE)); + current_key = NULL; + current_css = NULL; + continue; + } + if (current_css != NULL) + g_string_append_c (current_css, ch); + } + if (current_key != NULL || current_css != NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "missing '}'"); + return FALSE; + } + } + + /* success */ + return TRUE; +} + +/** + * gs_css_get_markup_for_id: + * @self: a #GsCss + * @id: an ID, or %NULL for the default + * + * Gets the CSS markup for a specific ID. + * + * Returns: %TRUE for success + */ +const gchar * +gs_css_get_markup_for_id (GsCss *self, const gchar *id) +{ + if (id == NULL) + id = "tile"; + return g_hash_table_lookup (self->ids, id); +} + +static void +_css_parsing_error_cb (GtkCssProvider *provider, + GtkCssSection *section, + GError *error, + gpointer user_data) +{ + GError **error_parse = (GError **) user_data; + if (*error_parse != NULL) { + g_warning ("ignoring parse error %u:%u: %s", + gtk_css_section_get_start_line (section), + gtk_css_section_get_start_position (section), + error->message); + return; + } + *error_parse = g_error_copy (error); +} + +static gboolean +gs_css_validate_part (GsCss *self, const gchar *markup, GError **error) +{ + g_autofree gchar *markup_new = NULL; + g_autoptr(GError) error_parse = NULL; + g_autoptr(GString) str = NULL; + g_autoptr(GtkCssProvider) provider = NULL; + + /* nothing set */ + if (markup == NULL) + return TRUE; + + /* remove custom class if NULL */ + str = g_string_new (NULL); + g_string_append (str, ".themed-widget {"); + if (self->rewrite_func != NULL) { + markup_new = self->rewrite_func (self->rewrite_func_data, + markup, + error); + if (markup_new == NULL) + return FALSE; + } else { + markup_new = g_strdup (markup); + } + g_string_append (str, markup_new); + g_string_append (str, "}"); + + /* set up custom provider */ + provider = gtk_css_provider_new (); + g_signal_connect (provider, "parsing-error", + G_CALLBACK (_css_parsing_error_cb), &error_parse); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_css_provider_load_from_data (provider, str->str, -1, NULL); + if (error_parse != NULL) { + if (error != NULL) + *error = g_error_copy (error_parse); + return FALSE; + } + return TRUE; +} + +/** + * gs_css_validate: + * @self: a #GsCss + * @error: a #GError or %NULL + * + * Validates each part of the CSS markup. + * + * Returns: %TRUE for success + */ +gboolean +gs_css_validate (GsCss *self, GError **error) +{ + g_autoptr(GList) keys = NULL; + + /* check each CSS ID */ + keys = g_hash_table_get_keys (self->ids); + for (GList *l = keys; l != NULL; l = l->next) { + const gchar *tmp; + const gchar *id = l->data; + if (g_strcmp0 (id, "tile") != 0 && + g_strcmp0 (id, "name") != 0 && + g_strcmp0 (id, "summary") != 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid CSS ID '%s'", + id); + return FALSE; + } + tmp = g_hash_table_lookup (self->ids, id); + if (!gs_css_validate_part (self, tmp, error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +/** + * gs_css_set_rewrite_func: + * @self: a #GsCss + * @func: a #GsCssRewriteFunc or %NULL + * @user_data: user data to pass to @func + * + * Sets a function to be used when rewriting CSS before it is parsed. + * + * Returns: %TRUE for success + */ +void +gs_css_set_rewrite_func (GsCss *self, GsCssRewriteFunc func, gpointer user_data) +{ + self->rewrite_func = func; + self->rewrite_func_data = user_data; +} + +static void +gs_css_finalize (GObject *object) +{ + GsCss *self = GS_CSS (object); + g_hash_table_unref (self->ids); + G_OBJECT_CLASS (gs_css_parent_class)->finalize (object); +} + +static void +gs_css_class_init (GsCssClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_css_finalize; +} + +static void +gs_css_init (GsCss *self) +{ + self->ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); +} + +/** + * gs_css_new: + * + * Return value: a new #GsCss object. + **/ +GsCss * +gs_css_new (void) +{ + GsCss *self; + self = g_object_new (GS_TYPE_CSS, NULL); + return GS_CSS (self); +} diff --git a/src/gs-css.h b/src/gs-css.h new file mode 100644 index 0000000..d2041a9 --- /dev/null +++ b/src/gs-css.h @@ -0,0 +1,36 @@ + /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GS_TYPE_CSS (gs_css_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCss, gs_css, GS, CSS, GObject) + +typedef gchar *(*GsCssRewriteFunc) (gpointer user_data, + const gchar *markup, + GError **error); + +GsCss *gs_css_new (void); +const gchar *gs_css_get_markup_for_id (GsCss *self, + const gchar *id); +gboolean gs_css_parse (GsCss *self, + const gchar *markup, + GError **error); +gboolean gs_css_validate (GsCss *self, + GError **error); +void gs_css_set_rewrite_func (GsCss *self, + GsCssRewriteFunc func, + gpointer user_data); + +G_END_DECLS diff --git a/src/gs-dbus-helper.c b/src/gs-dbus-helper.c new file mode 100644 index 0000000..b90ff98 --- /dev/null +++ b/src/gs-dbus-helper.c @@ -0,0 +1,814 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gio/gdesktopappinfo.h> +#include <gio/gio.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <packagekit-glib2/packagekit.h> + +#include "gnome-software-private.h" + +#include "gs-dbus-helper.h" +#include "gs-packagekit-generated.h" +#include "gs-packagekit-modify2-generated.h" +#include "gs-resources.h" +#include "gs-extras-page.h" + +struct _GsDbusHelper { + GObject parent; + GCancellable *cancellable; + GDBusInterfaceSkeleton *query_interface; + GDBusInterfaceSkeleton *modify_interface; + GDBusInterfaceSkeleton *modify2_interface; + PkTask *task; + guint dbus_own_name_id; +}; + +G_DEFINE_TYPE (GsDbusHelper, gs_dbus_helper, G_TYPE_OBJECT) + +typedef struct { + GDBusMethodInvocation *invocation; + GsDbusHelper *dbus_helper; + gboolean show_confirm_deps; + gboolean show_confirm_install; + gboolean show_confirm_search; + gboolean show_finished; + gboolean show_progress; + gboolean show_warning; +} GsDbusHelperTask; + +static void +gs_dbus_helper_task_free (GsDbusHelperTask *dtask) +{ + if (dtask->dbus_helper != NULL) + g_object_unref (dtask->dbus_helper); + + g_free (dtask); +} + +static void +gs_dbus_helper_task_set_interaction (GsDbusHelperTask *dtask, const gchar *interaction) +{ + guint i; + g_auto(GStrv) interactions = NULL; + + interactions = g_strsplit (interaction, ",", -1); + for (i = 0; interactions[i] != NULL; i++) { + if (g_strcmp0 (interactions[i], "show-warnings") == 0) + dtask->show_warning = TRUE; + else if (g_strcmp0 (interactions[i], "hide-warnings") == 0) + dtask->show_warning = FALSE; + else if (g_strcmp0 (interactions[i], "show-progress") == 0) + dtask->show_progress = TRUE; + else if (g_strcmp0 (interactions[i], "hide-progress") == 0) + dtask->show_progress = FALSE; + else if (g_strcmp0 (interactions[i], "show-finished") == 0) + dtask->show_finished = TRUE; + else if (g_strcmp0 (interactions[i], "hide-finished") == 0) + dtask->show_finished = FALSE; + else if (g_strcmp0 (interactions[i], "show-confirm-search") == 0) + dtask->show_confirm_search = TRUE; + else if (g_strcmp0 (interactions[i], "hide-confirm-search") == 0) + dtask->show_confirm_search = FALSE; + else if (g_strcmp0 (interactions[i], "show-confirm-install") == 0) + dtask->show_confirm_install = TRUE; + else if (g_strcmp0 (interactions[i], "hide-confirm-install") == 0) + dtask->show_confirm_install = FALSE; + else if (g_strcmp0 (interactions[i], "show-confirm-deps") == 0) + dtask->show_confirm_deps = TRUE; + else if (g_strcmp0 (interactions[i], "hide-confirm-deps") == 0) + dtask->show_confirm_deps = FALSE; + } +} + +static void +gs_dbus_helper_progress_cb (PkProgress *progress, PkProgressType type, gpointer data) +{ +} + +static void +gs_dbus_helper_query_is_installed_cb (GObject *source, GAsyncResult *res, gpointer data) +{ + GsDbusHelperTask *dtask = (GsDbusHelperTask *) data; + PkClient *client = PK_CLIENT (source); + g_autoptr(GError) error = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* get the results */ + results = pk_client_generic_finish (client, res, &error); + if (results == NULL) { + g_dbus_method_invocation_return_error (dtask->invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "failed to resolve: %s", + error->message); + goto out; + } + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_dbus_method_invocation_return_error (dtask->invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "failed to resolve: %s", + pk_error_get_details (error_code)); + goto out; + } + + /* get results */ + array = pk_results_get_package_array (results); + gs_package_kit_query_complete_is_installed (GS_PACKAGE_KIT_QUERY (dtask->dbus_helper->query_interface), + dtask->invocation, + array->len > 0); +out: + gs_dbus_helper_task_free (dtask); +} + +static void +gs_dbus_helper_query_search_file_cb (GObject *source, GAsyncResult *res, gpointer data) +{ + g_autoptr(GError) error = NULL; + GsDbusHelperTask *dtask = (GsDbusHelperTask *) data; + PkClient *client = PK_CLIENT (source); + PkInfoEnum info; + PkPackage *item; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(PkResults) results = NULL; + + /* get the results */ + results = pk_client_generic_finish (client, res, &error); + if (results == NULL) { + g_dbus_method_invocation_return_error (dtask->invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "failed to search: %s", + error->message); + return; + } + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_dbus_method_invocation_return_error (dtask->invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "failed to search: %s", + pk_error_get_details (error_code)); + return; + } + + /* get results */ + array = pk_results_get_package_array (results); + if (array->len == 0) { + //TODO: org.freedesktop.PackageKit.Query.unknown + g_dbus_method_invocation_return_error (dtask->invocation, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "failed to find any packages"); + return; + } + + /* get first item */ + item = g_ptr_array_index (array, 0); + info = pk_package_get_info (item); + gs_package_kit_query_complete_search_file (GS_PACKAGE_KIT_QUERY (dtask->dbus_helper->query_interface), + dtask->invocation, + info == PK_INFO_ENUM_INSTALLED, + pk_package_get_name (item)); +} + +static gboolean +handle_query_search_file (GsPackageKitQuery *skeleton, + GDBusMethodInvocation *invocation, + const gchar *file_name, + const gchar *interaction, + gpointer user_data) +{ + GsDbusHelper *dbus_helper = user_data; + GsDbusHelperTask *dtask; + g_auto(GStrv) names = NULL; + + g_debug ("****** SearchFile"); + + dtask = g_new0 (GsDbusHelperTask, 1); + dtask->dbus_helper = g_object_ref (dbus_helper); + dtask->invocation = invocation; + gs_dbus_helper_task_set_interaction (dtask, interaction); + names = g_strsplit (file_name, "&", -1); + pk_client_search_files_async (PK_CLIENT (dbus_helper->task), + pk_bitfield_value (PK_FILTER_ENUM_NEWEST), + names, NULL, + gs_dbus_helper_progress_cb, dtask, + gs_dbus_helper_query_search_file_cb, dtask); + + return TRUE; +} + +static gboolean +handle_query_is_installed (GsPackageKitQuery *skeleton, + GDBusMethodInvocation *invocation, + const gchar *package_name, + const gchar *interaction, + gpointer user_data) +{ + GsDbusHelper *dbus_helper = user_data; + GsDbusHelperTask *dtask; + g_auto(GStrv) names = NULL; + + g_debug ("****** IsInstalled"); + + dtask = g_new0 (GsDbusHelperTask, 1); + dtask->dbus_helper = g_object_ref (dbus_helper); + dtask->invocation = invocation; + gs_dbus_helper_task_set_interaction (dtask, interaction); + names = g_strsplit (package_name, "|", 1); + pk_client_resolve_async (PK_CLIENT (dbus_helper->task), + pk_bitfield_value (PK_FILTER_ENUM_INSTALLED), + names, NULL, + gs_dbus_helper_progress_cb, dtask, + gs_dbus_helper_query_is_installed_cb, dtask); + + return TRUE; +} + +static gboolean +is_show_confirm_search_set (const gchar *interaction) +{ + GsDbusHelperTask *dtask; + gboolean ret; + + dtask = g_new0 (GsDbusHelperTask, 1); + dtask->show_confirm_search = TRUE; + gs_dbus_helper_task_set_interaction (dtask, interaction); + ret = dtask->show_confirm_search; + gs_dbus_helper_task_free (dtask); + + return ret; +} + +static void +notify_search_resources (GsExtrasPageMode mode, + const gchar *desktop_id, + gchar **resources) +{ + const gchar *app_name = NULL; + const gchar *mode_string; + const gchar *title = NULL; + g_autofree gchar *body = NULL; + g_autoptr(GDesktopAppInfo) app_info = NULL; + g_autoptr(GNotification) n = NULL; + + if (desktop_id != NULL) { + app_info = gs_utils_get_desktop_app_info (desktop_id); + if (app_info != NULL) + app_name = g_app_info_get_name (G_APP_INFO (app_info)); + } + + if (app_name == NULL) { + /* TRANSLATORS: this is a what we use in notifications if the app's name is unknown */ + app_name = _("An application"); + } + + switch (mode) { + case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES: + /* TRANSLATORS: this is a notification displayed when an app needs additional MIME types. */ + body = g_strdup_printf (_("%s is requesting additional file format support."), app_name); + /* TRANSLATORS: notification title */ + title = _("Additional MIME Types Required"); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES: + /* TRANSLATORS: this is a notification displayed when an app needs additional fonts. */ + body = g_strdup_printf (_("%s is requesting additional fonts."), app_name); + /* TRANSLATORS: notification title */ + title = _("Additional Fonts Required"); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES: + /* TRANSLATORS: this is a notification displayed when an app needs additional codecs. */ + body = g_strdup_printf (_("%s is requesting additional multimedia codecs."), app_name); + /* TRANSLATORS: notification title */ + title = _("Additional Multimedia Codecs Required"); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS: + /* TRANSLATORS: this is a notification displayed when an app needs additional printer drivers. */ + body = g_strdup_printf (_("%s is requesting additional printer drivers."), app_name); + /* TRANSLATORS: notification title */ + title = _("Additional Printer Drivers Required"); + break; + default: + /* TRANSLATORS: this is a notification displayed when an app wants to install additional packages. */ + body = g_strdup_printf (_("%s is requesting additional packages."), app_name); + /* TRANSLATORS: notification title */ + title = _("Additional Packages Required"); + break; + } + + mode_string = gs_extras_page_mode_to_string (mode); + + n = g_notification_new (title); + g_notification_set_body (n, body); + /* TRANSLATORS: this is a button that launches gnome-software */ + g_notification_add_button_with_target (n, _("Find in Software"), "app.install-resources", "(s^ass)", mode_string, resources, ""); + g_notification_set_default_action_and_target (n, "app.install-resources", "(s^ass)", mode_string, resources, ""); + g_application_send_notification (g_application_get_default (), "install-resources", n); +} + +static void +install_resources (GsExtrasPageMode mode, + gchar **resources, + const gchar *interaction, + const gchar *desktop_id, + GVariant *platform_data) +{ + GApplication *app; + const gchar *mode_string; + const gchar *startup_id = NULL; + + if (is_show_confirm_search_set (interaction)) { + notify_search_resources (mode, desktop_id, resources); + return; + } + + if (platform_data != NULL) { + g_variant_lookup (platform_data, "desktop-startup-id", + "&s", &startup_id); + } + + app = g_application_get_default (); + mode_string = gs_extras_page_mode_to_string (mode); + g_action_group_activate_action (G_ACTION_GROUP (app), "install-resources", + g_variant_new ("(s^ass)", mode_string, resources, startup_id)); +} + +static gboolean +handle_modify_install_package_files (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_files, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallPackageFiles"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES, NULL, arg_files); + gs_package_kit_modify_complete_install_package_files (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_provide_files (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_files, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallProvideFiles"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES, NULL, arg_files); + gs_package_kit_modify_complete_install_provide_files (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_package_names (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_package_names, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallPackageNames"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES, NULL, arg_package_names); + gs_package_kit_modify_complete_install_package_names (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_mime_types (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_mime_types, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallMimeTypes"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES, NULL, arg_mime_types); + gs_package_kit_modify_complete_install_mime_types (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_fontconfig_resources (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_resources, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallFontconfigResources"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES, NULL, arg_resources); + gs_package_kit_modify_complete_install_fontconfig_resources (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_gstreamer_resources (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_resources, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallGStreamerResources"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES, NULL, arg_resources); + gs_package_kit_modify_complete_install_gstreamer_resources (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify_install_resources (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + const gchar *arg_type, + gchar **arg_resources, + const gchar *arg_interaction, + gpointer user_data) +{ + gboolean ret; + + g_debug ("****** Modify.InstallResources"); + + if (g_strcmp0 (arg_type, "plasma-service") == 0) { + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES, NULL, arg_resources); + ret = TRUE; + } else { + ret = FALSE; + } + gs_package_kit_modify_complete_install_resources (object, invocation); + + return ret; +} + +static gboolean +handle_modify_install_printer_drivers (GsPackageKitModify *object, + GDBusMethodInvocation *invocation, + guint arg_xid, + gchar **arg_device_ids, + const gchar *arg_interaction, + gpointer user_data) +{ + g_debug ("****** Modify.InstallPrinterDrivers"); + + notify_search_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS, NULL, arg_device_ids); + gs_package_kit_modify_complete_install_printer_drivers (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_package_files (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_files, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallPackageFiles"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES, arg_files, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_package_files (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_provide_files (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_files, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallProvideFiles"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES, arg_files, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_provide_files (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_package_names (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_package_names, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallPackageNames"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES, arg_package_names, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_package_names (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_mime_types (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_mime_types, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallMimeTypes"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES, arg_mime_types, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_mime_types (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_fontconfig_resources (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_resources, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallFontconfigResources"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES, arg_resources, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_fontconfig_resources (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_gstreamer_resources (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_resources, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallGStreamerResources"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES, arg_resources, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_gstreamer_resources (object, invocation); + + return TRUE; +} + +static gboolean +handle_modify2_install_resources (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + const gchar *arg_type, + gchar **arg_resources, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + gboolean ret; + + g_debug ("****** Modify2.InstallResources"); + + if (g_strcmp0 (arg_type, "plasma-service") == 0) { + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES, arg_resources, arg_interaction, arg_desktop_id, arg_platform_data); + ret = TRUE; + } else { + ret = FALSE; + } + gs_package_kit_modify2_complete_install_resources (object, invocation); + + return ret; +} + +static gboolean +handle_modify2_install_printer_drivers (GsPackageKitModify2 *object, + GDBusMethodInvocation *invocation, + gchar **arg_device_ids, + const gchar *arg_interaction, + const gchar *arg_desktop_id, + GVariant *arg_platform_data, + gpointer user_data) +{ + g_debug ("****** Modify2.InstallPrinterDrivers"); + + install_resources (GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS, arg_device_ids, arg_interaction, arg_desktop_id, arg_platform_data); + gs_package_kit_modify2_complete_install_printer_drivers (object, invocation); + + return TRUE; +} + +static void +gs_dbus_helper_name_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("acquired session service"); +} + +static void +gs_dbus_helper_name_lost_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_warning ("lost session service"); +} + +static void +bus_gotten_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsDbusHelper *dbus_helper = GS_DBUS_HELPER (user_data); + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(GDesktopAppInfo) app_info = NULL; + g_autoptr(GError) error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + return; + } + + /* Query interface */ + dbus_helper->query_interface = G_DBUS_INTERFACE_SKELETON (gs_package_kit_query_skeleton_new ()); + + g_signal_connect (dbus_helper->query_interface, "handle-is-installed", + G_CALLBACK (handle_query_is_installed), dbus_helper); + g_signal_connect (dbus_helper->query_interface, "handle-search-file", + G_CALLBACK (handle_query_search_file), dbus_helper); + + if (!g_dbus_interface_skeleton_export (dbus_helper->query_interface, + connection, + "/org/freedesktop/PackageKit", + &error)) { + g_warning ("Could not export dbus interface: %s", error->message); + return; + } + + /* Modify interface */ + dbus_helper->modify_interface = G_DBUS_INTERFACE_SKELETON (gs_package_kit_modify_skeleton_new ()); + + g_signal_connect (dbus_helper->modify_interface, "handle-install-package-files", + G_CALLBACK (handle_modify_install_package_files), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-provide-files", + G_CALLBACK (handle_modify_install_provide_files), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-package-names", + G_CALLBACK (handle_modify_install_package_names), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-mime-types", + G_CALLBACK (handle_modify_install_mime_types), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-fontconfig-resources", + G_CALLBACK (handle_modify_install_fontconfig_resources), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-gstreamer-resources", + G_CALLBACK (handle_modify_install_gstreamer_resources), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-resources", + G_CALLBACK (handle_modify_install_resources), dbus_helper); + g_signal_connect (dbus_helper->modify_interface, "handle-install-printer-drivers", + G_CALLBACK (handle_modify_install_printer_drivers), dbus_helper); + + if (!g_dbus_interface_skeleton_export (dbus_helper->modify_interface, + connection, + "/org/freedesktop/PackageKit", + &error)) { + g_warning ("Could not export dbus interface: %s", error->message); + return; + } + + /* Modify2 interface */ + dbus_helper->modify2_interface = G_DBUS_INTERFACE_SKELETON (gs_package_kit_modify2_skeleton_new ()); + + g_signal_connect (dbus_helper->modify2_interface, "handle-install-package-files", + G_CALLBACK (handle_modify2_install_package_files), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-provide-files", + G_CALLBACK (handle_modify2_install_provide_files), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-package-names", + G_CALLBACK (handle_modify2_install_package_names), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-mime-types", + G_CALLBACK (handle_modify2_install_mime_types), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-fontconfig-resources", + G_CALLBACK (handle_modify2_install_fontconfig_resources), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-gstreamer-resources", + G_CALLBACK (handle_modify2_install_gstreamer_resources), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-resources", + G_CALLBACK (handle_modify2_install_resources), dbus_helper); + g_signal_connect (dbus_helper->modify2_interface, "handle-install-printer-drivers", + G_CALLBACK (handle_modify2_install_printer_drivers), dbus_helper); + + /* Look up our own localized name and export it as a property on the bus */ + app_info = g_desktop_app_info_new ("org.gnome.Software.desktop"); + if (app_info != NULL) { + const gchar *app_name = g_app_info_get_name (G_APP_INFO (app_info)); + if (app_name != NULL) + g_object_set (G_OBJECT (dbus_helper->modify2_interface), + "display-name", app_name, + NULL); + } + + if (!g_dbus_interface_skeleton_export (dbus_helper->modify2_interface, + connection, + "/org/freedesktop/PackageKit", + &error)) { + g_warning ("Could not export dbus interface: %s", error->message); + return; + } + + dbus_helper->dbus_own_name_id = g_bus_own_name_on_connection (connection, + "org.freedesktop.PackageKit", + G_BUS_NAME_OWNER_FLAGS_NONE, + gs_dbus_helper_name_acquired_cb, + gs_dbus_helper_name_lost_cb, + NULL, NULL); +} + +static void +gs_dbus_helper_init (GsDbusHelper *dbus_helper) +{ + dbus_helper->task = pk_task_new (); + dbus_helper->cancellable = g_cancellable_new (); + + g_bus_get (G_BUS_TYPE_SESSION, + dbus_helper->cancellable, + (GAsyncReadyCallback) bus_gotten_cb, + dbus_helper); +} + +static void +gs_dbus_helper_dispose (GObject *object) +{ + GsDbusHelper *dbus_helper = GS_DBUS_HELPER (object); + + g_cancellable_cancel (dbus_helper->cancellable); + g_clear_object (&dbus_helper->cancellable); + + if (dbus_helper->dbus_own_name_id != 0) { + g_bus_unown_name (dbus_helper->dbus_own_name_id); + dbus_helper->dbus_own_name_id = 0; + } + + if (dbus_helper->query_interface != NULL) { + g_dbus_interface_skeleton_unexport (dbus_helper->query_interface); + g_clear_object (&dbus_helper->query_interface); + } + + if (dbus_helper->modify_interface != NULL) { + g_dbus_interface_skeleton_unexport (dbus_helper->modify_interface); + g_clear_object (&dbus_helper->modify_interface); + } + + if (dbus_helper->modify2_interface != NULL) { + g_dbus_interface_skeleton_unexport (dbus_helper->modify2_interface); + g_clear_object (&dbus_helper->modify2_interface); + } + + g_clear_object (&dbus_helper->task); + + G_OBJECT_CLASS (gs_dbus_helper_parent_class)->dispose (object); +} + +static void +gs_dbus_helper_class_init (GsDbusHelperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_dbus_helper_dispose; +} + +GsDbusHelper * +gs_dbus_helper_new (void) +{ + return GS_DBUS_HELPER (g_object_new (GS_TYPE_DBUS_HELPER, NULL)); +} diff --git a/src/gs-dbus-helper.h b/src/gs-dbus-helper.h new file mode 100644 index 0000000..7e77ea5 --- /dev/null +++ b/src/gs-dbus-helper.h @@ -0,0 +1,22 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_DBUS_HELPER (gs_dbus_helper_get_type ()) + +G_DECLARE_FINAL_TYPE (GsDbusHelper, gs_dbus_helper, GS, DBUS_HELPER, GObject) + +GsDbusHelper *gs_dbus_helper_new (void); + +G_END_DECLS diff --git a/src/gs-details-page.c b/src/gs-details-page.c new file mode 100644 index 0000000..6994e6a --- /dev/null +++ b/src/gs-details-page.c @@ -0,0 +1,2856 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <locale.h> +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-common.h" +#include "gs-content-rating.h" +#include "gs-utils.h" + +#include "gs-details-page.h" +#include "gs-app-addon-row.h" +#include "gs-history-dialog.h" +#include "gs-origin-popover-row.h" +#include "gs-screenshot-image.h" +#include "gs-star-widget.h" +#include "gs-review-histogram.h" +#include "gs-review-dialog.h" +#include "gs-review-row.h" + +/* the number of reviews to show before clicking the 'More Reviews' button */ +#define SHOW_NR_REVIEWS_INITIAL 4 + +static void gs_details_page_refresh_all (GsDetailsPage *self); + +typedef enum { + GS_DETAILS_PAGE_STATE_LOADING, + GS_DETAILS_PAGE_STATE_READY, + GS_DETAILS_PAGE_STATE_FAILED +} GsDetailsPageState; + +struct _GsDetailsPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + GCancellable *app_cancellable; + GsApp *app; + GsApp *app_local_file; + GsShell *shell; + SoupSession *session; + gboolean enable_reviews; + gboolean show_all_reviews; + GSettings *settings; + GtkSizeGroup *size_group_origin_popover; + + GtkWidget *application_details_icon; + GtkWidget *application_details_summary; + GtkWidget *application_details_title; + GtkWidget *box_addons; + GtkWidget *box_details; + GtkWidget *box_details_description; + GtkWidget *box_details_support; + GtkWidget *box_progress; + GtkWidget *box_progress2; + GtkWidget *star; + GtkWidget *label_review_count; + GtkWidget *box_details_screenshot; + GtkWidget *box_details_screenshot_main; + GtkWidget *box_details_screenshot_scrolledwindow; + GtkWidget *box_details_screenshot_thumbnails; + GtkWidget *box_details_license_list; + GtkWidget *button_details_launch; + GtkWidget *button_details_add_shortcut; + GtkWidget *button_details_remove_shortcut; + GtkWidget *button_details_website; + GtkWidget *button_donate; + GtkWidget *button_install; + GtkWidget *button_update; + GtkWidget *button_remove; + GtkWidget *button_cancel; + GtkWidget *button_more_reviews; + GtkWidget *infobar_details_app_norepo; + GtkWidget *infobar_details_app_repo; + GtkWidget *infobar_details_package_baseos; + GtkWidget *infobar_details_repo; + GtkWidget *label_progress_percentage; + GtkWidget *label_progress_status; + GtkWidget *label_addons_uninstalled_app; + GtkWidget *label_details_category_title; + GtkWidget *label_details_category_value; + GtkWidget *label_details_developer_title; + GtkWidget *label_details_developer_value; + GtkWidget *box_details_developer; + GtkWidget *image_details_developer_verified; + GtkWidget *button_details_license_free; + GtkWidget *button_details_license_nonfree; + GtkWidget *button_details_license_unknown; + GtkWidget *label_details_license_title; + GtkWidget *box_details_license_value; + GtkWidget *label_details_channel_title; + GtkWidget *label_details_channel_value; + GtkWidget *label_details_origin_title; + GtkWidget *label_details_origin_value; + GtkWidget *label_details_size_installed_title; + GtkWidget *label_details_size_installed_value; + GtkWidget *label_details_size_download_title; + GtkWidget *label_details_size_download_value; + GtkWidget *label_details_updated_title; + GtkWidget *label_details_updated_value; + GtkWidget *label_details_version_title; + GtkWidget *label_details_version_value; + GtkWidget *label_details_permissions_title; + GtkWidget *button_details_permissions_value; + GtkWidget *label_failed; + GtkWidget *label_license_nonfree_details; + GtkWidget *label_licenses_intro; + GtkWidget *list_box_addons; + GtkWidget *box_reviews; + GtkWidget *box_details_screenshot_fallback; + GtkWidget *histogram; + GtkWidget *button_review; + GtkWidget *list_box_reviews; + GtkWidget *scrolledwindow_details; + GtkWidget *spinner_details; + GtkWidget *spinner_remove; + GtkWidget *stack_details; + GtkWidget *grid_details_kudo; + GtkWidget *image_details_kudo_docs; + GtkWidget *image_details_kudo_sandboxed; + GtkWidget *image_details_kudo_integration; + GtkWidget *image_details_kudo_translated; + GtkWidget *image_details_kudo_updated; + GtkWidget *label_details_kudo_docs; + GtkWidget *label_details_kudo_sandboxed; + GtkWidget *label_details_kudo_integration; + GtkWidget *label_details_kudo_translated; + GtkWidget *label_details_kudo_updated; + GtkWidget *progressbar_top; + guint progress_pulse_id; + GtkWidget *popover_license_free; + GtkWidget *popover_license_nonfree; + GtkWidget *popover_license_unknown; + GtkWidget *popover_content_rating; + GtkWidget *label_content_rating_title; + GtkWidget *label_content_rating_message; + GtkWidget *label_content_rating_none; + GtkWidget *button_details_rating_value; + GtkStyleProvider *button_details_rating_style_provider; + GtkWidget *label_details_rating_title; + GtkWidget *popover_permissions; + GtkWidget *box_permissions_details; + GtkWidget *star_eventbox; +}; + +G_DEFINE_TYPE (GsDetailsPage, gs_details_page, GS_TYPE_PAGE) + +static void +gs_details_page_set_state (GsDetailsPage *self, + GsDetailsPageState state) +{ + /* spinner */ + switch (state) { + case GS_DETAILS_PAGE_STATE_LOADING: + gs_start_spinner (GTK_SPINNER (self->spinner_details)); + gtk_widget_show (self->spinner_details); + break; + case GS_DETAILS_PAGE_STATE_READY: + case GS_DETAILS_PAGE_STATE_FAILED: + gs_stop_spinner (GTK_SPINNER (self->spinner_details)); + gtk_widget_hide (self->spinner_details); + break; + default: + g_assert_not_reached (); + } + + /* stack */ + switch (state) { + case GS_DETAILS_PAGE_STATE_LOADING: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "spinner"); + break; + case GS_DETAILS_PAGE_STATE_READY: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "ready"); + break; + case GS_DETAILS_PAGE_STATE_FAILED: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_details), "failed"); + break; + default: + g_assert_not_reached (); + } +} + +static void +gs_details_page_update_shortcut_button (GsDetailsPage *self) +{ + gboolean add_shortcut_func; + gboolean remove_shortcut_func; + gboolean has_shortcut; + + gtk_widget_set_visible (self->button_details_add_shortcut, + FALSE); + gtk_widget_set_visible (self->button_details_remove_shortcut, + FALSE); + + if (gs_app_get_kind (self->app) != AS_APP_KIND_DESKTOP) + return; + + /* Leave the button hidden if the app can’t be launched by the current + * user. */ + if (gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE)) + return; + + /* only consider the shortcut button if the app is installed */ + switch (gs_app_get_state (self->app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + break; + default: + return; + } + + add_shortcut_func = + gs_plugin_loader_get_plugin_supported (self->plugin_loader, + "gs_plugin_add_shortcut"); + remove_shortcut_func = + gs_plugin_loader_get_plugin_supported (self->plugin_loader, + "gs_plugin_remove_shortcut"); + + has_shortcut = gs_app_has_quirk (self->app, GS_APP_QUIRK_HAS_SHORTCUT); + + if (add_shortcut_func) { + gtk_widget_set_visible (self->button_details_add_shortcut, + !has_shortcut || !remove_shortcut_func); + gtk_widget_set_sensitive (self->button_details_add_shortcut, + !has_shortcut); + } + + if (remove_shortcut_func) { + gtk_widget_set_visible (self->button_details_remove_shortcut, + has_shortcut || !add_shortcut_func); + gtk_widget_set_sensitive (self->button_details_remove_shortcut, + has_shortcut); + } +} + +static gboolean +app_has_pending_action (GsApp *app) +{ + /* sanitize the pending state change by verifying we're in one of the + * expected states */ + if (gs_app_get_state (app) != AS_APP_STATE_AVAILABLE && + gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE && + gs_app_get_state (app) != AS_APP_STATE_UPDATABLE && + gs_app_get_state (app) != AS_APP_STATE_QUEUED_FOR_INSTALL) + return FALSE; + + return (gs_app_get_pending_action (app) != GS_PLUGIN_ACTION_UNKNOWN) || + (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL); +} + +static void +gs_details_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (page); + GtkWidget *widget; + GtkAdjustment *adj; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_DETAILS) { + g_warning ("Called switch_to(details) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header")); + gtk_label_set_label (GTK_LABEL (widget), ""); + gtk_widget_show (widget); + + /* not set, perhaps file-to-app */ + if (self->app == NULL) + return; + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + + gs_grab_focus_when_mapped (self->scrolledwindow_details); +} + +static gboolean +_pulse_cb (gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + + gtk_progress_bar_pulse (GTK_PROGRESS_BAR (self->progressbar_top)); + + return G_SOURCE_CONTINUE; +} + +static void +stop_progress_pulsing (GsDetailsPage *self) +{ + if (self->progress_pulse_id != 0) { + g_source_remove (self->progress_pulse_id); + self->progress_pulse_id = 0; + } +} + +static void +gs_details_page_refresh_progress (GsDetailsPage *self) +{ + guint percentage; + AsAppState state; + + /* cancel button */ + state = gs_app_get_state (self->app); + switch (state) { + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (self->button_cancel, TRUE); + /* If the app is installing, the user can only cancel it if + * 1) They haven't already, and + * 2) the plugin hasn't said that they can't, for example if a + * package manager has already gone 'too far' + */ + gtk_widget_set_sensitive (self->button_cancel, + !g_cancellable_is_cancelled (self->app_cancellable) && + gs_app_get_allow_cancel (self->app)); + break; + default: + gtk_widget_set_visible (self->button_cancel, FALSE); + break; + } + if (app_has_pending_action (self->app)) { + gtk_widget_set_visible (self->button_cancel, TRUE); + gtk_widget_set_sensitive (self->button_cancel, + !g_cancellable_is_cancelled (self->app_cancellable) && + gs_app_get_allow_cancel (self->app)); + } + + /* progress status label */ + switch (state) { + case AS_APP_STATE_REMOVING: + gtk_widget_set_visible (self->label_progress_status, TRUE); + gtk_label_set_label (GTK_LABEL (self->label_progress_status), + _("Removing…")); + break; + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (self->label_progress_status, TRUE); + gtk_label_set_label (GTK_LABEL (self->label_progress_status), + _("Installing")); + break; + default: + gtk_widget_set_visible (self->label_progress_status, FALSE); + break; + } + if (app_has_pending_action (self->app)) { + GsPluginAction action = gs_app_get_pending_action (self->app); + gtk_widget_set_visible (self->label_progress_status, TRUE); + switch (action) { + case GS_PLUGIN_ACTION_INSTALL: + gtk_label_set_label (GTK_LABEL (self->label_progress_status), + /* TRANSLATORS: This is a label on top of the app's progress + * bar to inform the user that the app should be installed soon */ + _("Pending installation…")); + break; + case GS_PLUGIN_ACTION_UPDATE: + case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: + gtk_label_set_label (GTK_LABEL (self->label_progress_status), + /* TRANSLATORS: This is a label on top of the app's progress + * bar to inform the user that the app should be updated soon */ + _("Pending update…")); + break; + default: + gtk_widget_set_visible (self->label_progress_status, FALSE); + break; + } + } + + /* percentage bar */ + switch (state) { + case AS_APP_STATE_INSTALLING: + percentage = gs_app_get_progress (self->app); + if (percentage == GS_APP_PROGRESS_UNKNOWN) { + /* Translators: This string is shown when preparing to download and install an app. */ + gtk_label_set_label (GTK_LABEL (self->label_progress_status), _("Preparing…")); + gtk_widget_set_visible (self->label_progress_status, TRUE); + gtk_widget_set_visible (self->label_progress_percentage, FALSE); + + if (self->progress_pulse_id == 0) + self->progress_pulse_id = g_timeout_add (50, _pulse_cb, self); + + gtk_widget_set_visible (self->progressbar_top, TRUE); + break; + } else if (percentage <= 100) { + g_autofree gchar *str = g_strdup_printf ("%u%%", percentage); + gtk_label_set_label (GTK_LABEL (self->label_progress_percentage), str); + gtk_widget_set_visible (self->label_progress_percentage, TRUE); + stop_progress_pulsing (self); + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progressbar_top), + (gdouble) percentage / 100.f); + gtk_widget_set_visible (self->progressbar_top, TRUE); + break; + } + /* FALLTHROUGH */ + default: + gtk_widget_set_visible (self->label_progress_percentage, FALSE); + gtk_widget_set_visible (self->progressbar_top, FALSE); + stop_progress_pulsing (self); + break; + } + if (app_has_pending_action (self->app)) { + gtk_widget_set_visible (self->progressbar_top, TRUE); + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progressbar_top), 0); + } + + /* spinner */ + switch (state) { + case AS_APP_STATE_REMOVING: + if (!gtk_widget_get_visible (self->spinner_remove)) { + gtk_spinner_start (GTK_SPINNER (self->spinner_remove)); + gtk_widget_set_visible (self->spinner_remove, TRUE); + } + /* align text together with the spinner if we're showing it */ + gtk_widget_set_halign (self->box_progress2, GTK_ALIGN_START); + break; + default: + gtk_widget_set_visible (self->spinner_remove, FALSE); + gtk_spinner_stop (GTK_SPINNER (self->spinner_remove)); + gtk_widget_set_halign (self->box_progress2, GTK_ALIGN_CENTER); + break; + } + + /* progress box */ + switch (state) { + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (self->box_progress, TRUE); + break; + default: + gtk_widget_set_visible (self->box_progress, FALSE); + break; + } + if (app_has_pending_action (self->app)) + gtk_widget_set_visible (self->box_progress, TRUE); +} + +static gboolean +gs_details_page_refresh_progress_idle (gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + gs_details_page_refresh_progress (self); + g_object_unref (self); + return G_SOURCE_REMOVE; +} + +static void +gs_details_page_progress_changed_cb (GsApp *app, + GParamSpec *pspec, + GsDetailsPage *self) +{ + g_idle_add (gs_details_page_refresh_progress_idle, g_object_ref (self)); +} + +static gboolean +gs_details_page_allow_cancel_changed_idle (gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + gtk_widget_set_sensitive (self->button_cancel, + gs_app_get_allow_cancel (self->app)); + g_object_unref (self); + return G_SOURCE_REMOVE; +} + +static void +gs_details_page_allow_cancel_changed_cb (GsApp *app, + GParamSpec *pspec, + GsDetailsPage *self) +{ + g_idle_add (gs_details_page_allow_cancel_changed_idle, + g_object_ref (self)); +} + +static gboolean +gs_details_page_switch_to_idle (gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + + if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_DETAILS) + gs_page_switch_to (GS_PAGE (self), TRUE); + + /* update widgets */ + gs_details_page_refresh_all (self); + + g_object_unref (self); + return G_SOURCE_REMOVE; +} + +static void +gs_details_page_notify_state_changed_cb (GsApp *app, + GParamSpec *pspec, + GsDetailsPage *self) +{ + g_idle_add (gs_details_page_switch_to_idle, g_object_ref (self)); +} + +static void +gs_details_page_load_main_screenshot (GsDetailsPage *self, + AsScreenshot *screenshot) +{ + GsScreenshotImage *ssmain; + g_autoptr(GList) children = NULL; + + children = gtk_container_get_children (GTK_CONTAINER (self->box_details_screenshot_main)); + ssmain = GS_SCREENSHOT_IMAGE (children->data); + + gs_screenshot_image_set_screenshot (ssmain, screenshot); + gs_screenshot_image_load_async (ssmain, NULL); +} + +static void +gs_details_page_screenshot_selected_cb (GtkListBox *list, + GtkListBoxRow *row, + GsDetailsPage *self) +{ + GsScreenshotImage *ssthumb; + AsScreenshot *ss; + g_autoptr(GList) children = NULL; + + if (row == NULL) + return; + + ssthumb = GS_SCREENSHOT_IMAGE (gtk_bin_get_child (GTK_BIN (row))); + ss = gs_screenshot_image_get_screenshot (ssthumb); + + gs_details_page_load_main_screenshot (self, ss); +} + +static void +gs_details_page_refresh_screenshots (GsDetailsPage *self) +{ + GPtrArray *screenshots; + AsScreenshot *ss; + GtkWidget *label; + GtkWidget *list; + GtkWidget *ssimg; + GtkWidget *main_screenshot = NULL; + guint i; + gboolean is_offline = !gs_plugin_loader_get_network_available (self->plugin_loader); + guint num_screenshots_loaded = 0; + + /* reset the visibility of screenshots */ + gtk_widget_show (self->box_details_screenshot); + + /* treat screenshots differently */ + if (gs_app_get_kind (self->app) == AS_APP_KIND_FONT) { + gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails)); + gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main)); + screenshots = gs_app_get_screenshots (self->app); + for (i = 0; i < screenshots->len; i++) { + ss = g_ptr_array_index (screenshots, i); + + /* set caption */ + label = gtk_label_new (as_screenshot_get_caption (ss, NULL)); + g_object_set (label, + "xalign", 0.0, + "max-width-chars", 10, + "wrap", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), label); + gtk_widget_set_visible (label, TRUE); + + /* set images */ + ssimg = gs_screenshot_image_new (self->session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), + 640, + 48); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL); + gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), ssimg); + gtk_widget_set_visible (ssimg, TRUE); + } + gtk_widget_set_visible (self->box_details_screenshot, + screenshots->len > 0); + gtk_widget_set_visible (self->box_details_screenshot_fallback, + screenshots->len == 0 && !is_offline); + return; + } + + /* fallback warning */ + screenshots = gs_app_get_screenshots (self->app); + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_GENERIC: + case AS_APP_KIND_CODEC: + case AS_APP_KIND_ADDON: + case AS_APP_KIND_SOURCE: + case AS_APP_KIND_FIRMWARE: + case AS_APP_KIND_DRIVER: + case AS_APP_KIND_INPUT_METHOD: + case AS_APP_KIND_LOCALIZATION: + case AS_APP_KIND_RUNTIME: + gtk_widget_set_visible (self->box_details_screenshot_fallback, FALSE); + break; + default: + gtk_widget_set_visible (self->box_details_screenshot_fallback, + screenshots->len == 0 && !is_offline); + break; + } + + /* reset screenshots */ + gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main)); + gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails)); + + list = gtk_list_box_new (); + gtk_style_context_add_class (gtk_widget_get_style_context (list), "image-list"); + gtk_widget_show (list); + gtk_widget_show (self->box_details_screenshot_scrolledwindow); + gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_thumbnails), list); + + for (i = 0; i < screenshots->len; i++) { + ss = g_ptr_array_index (screenshots, i); + + /* we need to load the main screenshot only once if we're online + * but all times if we're offline (to check which are cached and + * hide those who aren't) */ + if (is_offline || main_screenshot == NULL) { + GtkWidget *ssmain = gs_screenshot_image_new (self->session); + gtk_widget_set_can_focus (gtk_bin_get_child (GTK_BIN (ssmain)), FALSE); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssmain), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssmain), + AS_IMAGE_NORMAL_WIDTH, + AS_IMAGE_NORMAL_HEIGHT); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssmain), NULL); + + /* when we're offline, the load will be immediate, so we + * can check if it succeeded, and just skip it and its + * thumbnails otherwise */ + if (is_offline && + !gs_screenshot_image_is_showing (GS_SCREENSHOT_IMAGE (ssmain))) + continue; + + /* only set the main_screenshot once */ + if (main_screenshot == NULL) { + main_screenshot = ssmain; + gtk_box_pack_start (GTK_BOX (self->box_details_screenshot_main), + main_screenshot, FALSE, FALSE, 0); + gtk_widget_show (main_screenshot); + } + } + + ssimg = gs_screenshot_image_new (self->session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), + AS_IMAGE_THUMBNAIL_WIDTH, + AS_IMAGE_THUMBNAIL_HEIGHT); + gtk_style_context_add_class (gtk_widget_get_style_context (ssimg), + "screenshot-image-thumb"); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL); + gtk_list_box_insert (GTK_LIST_BOX (list), ssimg, -1); + gtk_widget_set_visible (ssimg, TRUE); + ++num_screenshots_loaded; + } + + if (main_screenshot == NULL) { + gtk_widget_hide (self->box_details_screenshot); + return; + } + + /* reload the main screenshot with a larger size if it's the only screenshot + * available */ + if (num_screenshots_loaded == 1) { + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (main_screenshot), + AS_IMAGE_LARGE_WIDTH, + AS_IMAGE_LARGE_HEIGHT); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (main_screenshot), NULL); + } + + if (num_screenshots_loaded <= 1) { + gtk_widget_hide (self->box_details_screenshot_thumbnails); + return; + } + + gtk_widget_show (self->box_details_screenshot_thumbnails); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (list), GTK_SELECTION_BROWSE); + g_signal_connect (list, "row-selected", + G_CALLBACK (gs_details_page_screenshot_selected_cb), + self); + gtk_list_box_select_row (GTK_LIST_BOX (list), + gtk_list_box_get_row_at_index (GTK_LIST_BOX (list), 0)); +} + +static void +gs_details_page_website_cb (GtkWidget *widget, GsDetailsPage *self) +{ + gs_shell_show_uri (self->shell, + gs_app_get_url (self->app, AS_URL_KIND_HOMEPAGE)); +} + +static void +gs_details_page_donate_cb (GtkWidget *widget, GsDetailsPage *self) +{ + gs_shell_show_uri (self->shell, gs_app_get_url (self->app, AS_URL_KIND_DONATION)); +} + +static void +gs_details_page_set_description (GsDetailsPage *self, const gchar *tmp) +{ + GtkStyleContext *style_context; + GtkWidget *para; + guint i; + g_auto(GStrv) split = NULL; + + /* does the description exist? */ + gtk_widget_set_visible (self->box_details_description, tmp != NULL); + if (tmp == NULL) + return; + + /* add each paragraph as a new GtkLabel which lets us get the 24px + * paragraph spacing */ + gs_container_remove_all (GTK_CONTAINER (self->box_details_description)); + split = g_strsplit (tmp, "\n\n", -1); + for (i = 0; split[i] != NULL; i++) { + para = gtk_label_new (split[i]); + gtk_label_set_line_wrap (GTK_LABEL (para), TRUE); + gtk_label_set_max_width_chars (GTK_LABEL (para), 40); + gtk_label_set_selectable (GTK_LABEL (para), TRUE); + gtk_widget_set_visible (para, TRUE); + gtk_widget_set_can_focus (para, FALSE); + g_object_set (para, + "xalign", 0.0, + NULL); + + /* add style class for theming */ + style_context = gtk_widget_get_style_context (para); + gtk_style_context_add_class (style_context, + "application-details-description"); + + gtk_container_add (GTK_CONTAINER (self->box_details_description), para); + } + + /* show the webapp warning */ + if (gs_app_get_kind (self->app) == AS_APP_KIND_WEB_APP) { + GtkWidget *label; + /* TRANSLATORS: this is the warning box */ + label = gtk_label_new (_("This application can only be used when there is an active internet connection.")); + gtk_widget_set_visible (label, TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.f); + gtk_style_context_add_class (gtk_widget_get_style_context (label), + "application-details-webapp-warning"); + gtk_container_add (GTK_CONTAINER (self->box_details_description), label); + } +} + +static void +gs_details_page_set_sensitive (GtkWidget *widget, gboolean is_active) +{ + GtkStyleContext *style_context; + style_context = gtk_widget_get_style_context (widget); + if (!is_active) { + gtk_style_context_add_class (style_context, "dim-label"); + } else { + gtk_style_context_remove_class (style_context, "dim-label"); + } +} + +static gboolean +gs_details_page_history_cb (GtkLabel *label, + gchar *uri, + GsDetailsPage *self) +{ + GtkWidget *dialog; + + dialog = gs_history_dialog_new (); + gs_history_dialog_set_app (GS_HISTORY_DIALOG (dialog), self->app); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); + + return TRUE; +} + +static void +gs_details_page_refresh_size (GsDetailsPage *self) +{ + /* set the installed size */ + if (gs_app_get_size_installed (self->app) != GS_APP_SIZE_UNKNOWABLE && + gs_app_get_size_installed (self->app) != 0) { + g_autofree gchar *size = NULL; + size = g_format_size (gs_app_get_size_installed (self->app)); + gtk_label_set_label (GTK_LABEL (self->label_details_size_installed_value), size); + gtk_widget_show (self->label_details_size_installed_title); + gtk_widget_show (self->label_details_size_installed_value); + } else { + gtk_widget_hide (self->label_details_size_installed_title); + gtk_widget_hide (self->label_details_size_installed_value); + } + + /* set the download size */ + if (!gs_app_is_installed (self->app) && + gs_app_get_size_download (self->app) != GS_APP_SIZE_UNKNOWABLE) { + g_autofree gchar *size = NULL; + size = g_format_size (gs_app_get_size_download (self->app)); + gtk_label_set_label (GTK_LABEL (self->label_details_size_download_value), size); + gtk_widget_show (self->label_details_size_download_title); + gtk_widget_show (self->label_details_size_download_value); + } else { + gtk_widget_hide (self->label_details_size_download_title); + gtk_widget_hide (self->label_details_size_download_value); + } +} + +static void +gs_details_page_get_alternates_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + GtkWidget *origin_box; + GtkWidget *origin_button_label; + GtkWidget *origin_popover_list_box; + g_autofree gchar *origin_ui = NULL; + + origin_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_box")); + origin_button_label = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_button_label")); + origin_popover_list_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover_list_box")); + + gs_container_remove_all (GTK_CONTAINER (origin_popover_list_box)); + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get alternates: %s", error->message); + gtk_widget_hide (origin_box); + return; + } + + /* add the local file to the list so that we can carry it over when + * switching between alternates */ + if (self->app_local_file != NULL) + gs_app_list_add (list, self->app_local_file); + + /* no alternates to show */ + if (gs_app_list_length (list) < 2) { + gtk_widget_hide (origin_box); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GtkWidget *row = gs_origin_popover_row_new (app); + gtk_widget_show (row); + if (app == self->app) + gs_origin_popover_row_set_selected (GS_ORIGIN_POPOVER_ROW (row), TRUE); + gs_origin_popover_row_set_size_group (GS_ORIGIN_POPOVER_ROW (row), + self->size_group_origin_popover); + gtk_container_add (GTK_CONTAINER (origin_popover_list_box), row); + } + + origin_ui = gs_app_get_origin_ui (self->app); + if (origin_ui != NULL) + gtk_label_set_text (GTK_LABEL (origin_button_label), origin_ui); + else + gtk_label_set_text (GTK_LABEL (origin_button_label), ""); + + gtk_widget_show (origin_box); +} + +static void +gs_details_page_refresh_buttons (GsDetailsPage *self) +{ + AsAppState state; + g_autofree gchar *text = NULL; + + state = gs_app_get_state (self->app); + + /* install button */ + switch (state) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + gtk_widget_set_visible (self->button_install, TRUE); + /* TRANSLATORS: button text in the header when an application + * can be installed */ + gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install")); + break; + case AS_APP_STATE_INSTALLING: + gtk_widget_set_visible (self->button_install, FALSE); + break; + case AS_APP_STATE_UNKNOWN: + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (self->button_install, FALSE); + break; + case AS_APP_STATE_UPDATABLE_LIVE: + if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) { + gtk_widget_set_visible (self->button_install, TRUE); + /* TRANSLATORS: button text in the header when firmware + * can be live-installed */ + gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install")); + } else { + gtk_widget_set_visible (self->button_install, FALSE); + } + break; + case AS_APP_STATE_UNAVAILABLE: + if (gs_app_get_url (self->app, AS_URL_KIND_MISSING) != NULL) { + gtk_widget_set_visible (self->button_install, FALSE); + } else { + gtk_widget_set_visible (self->button_install, TRUE); + /* TRANSLATORS: this is a button that allows the apps to + * be installed. + * The ellipsis indicates that further steps are required, + * e.g. enabling software repositories or the like */ + gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Install…")); + } + break; + default: + g_warning ("App unexpectedly in state %s", + as_app_state_to_string (state)); + g_assert_not_reached (); + } + + /* update button */ + switch (state) { + case AS_APP_STATE_UPDATABLE_LIVE: + if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) { + gtk_widget_set_visible (self->button_update, FALSE); + } else { + gtk_widget_set_visible (self->button_update, TRUE); + } + break; + default: + gtk_widget_set_visible (self->button_update, FALSE); + break; + } + + /* launch button */ + switch (gs_app_get_state (self->app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + if (!gs_app_has_quirk (self->app, GS_APP_QUIRK_NOT_LAUNCHABLE) && + !gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE)) { + gtk_widget_set_visible (self->button_details_launch, TRUE); + } else { + gtk_widget_set_visible (self->button_details_launch, FALSE); + } + break; + default: + gtk_widget_set_visible (self->button_details_launch, FALSE); + break; + } + + gtk_button_set_label (GTK_BUTTON (self->button_details_launch), + /* TRANSLATORS: A label for a button to execute the selected application. */ + _("_Launch")); + + /* don't show the launch and shortcut buttons if the app doesn't have a desktop ID */ + if (gs_app_get_id (self->app) == NULL) { + gtk_widget_set_visible (self->button_details_launch, FALSE); + } + + /* remove button */ + if (gs_app_has_quirk (self->app, GS_APP_QUIRK_COMPULSORY) || + gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) { + gtk_widget_set_visible (self->button_remove, FALSE); + } else { + switch (state) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + gtk_widget_set_visible (self->button_remove, TRUE); + gtk_widget_set_sensitive (self->button_remove, TRUE); + /* Mark the button as destructive only if Launch is not visible */ + if (gtk_widget_get_visible (self->button_details_launch)) + gtk_style_context_remove_class (gtk_widget_get_style_context (self->button_remove), "destructive-action"); + else + gtk_style_context_add_class (gtk_widget_get_style_context (self->button_remove), "destructive-action"); + /* TRANSLATORS: button text in the header when an application can be erased */ + gtk_button_set_label (GTK_BUTTON (self->button_remove), _("_Remove")); + break; + case AS_APP_STATE_AVAILABLE_LOCAL: + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_UNAVAILABLE: + case AS_APP_STATE_UNKNOWN: + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (self->button_remove, FALSE); + break; + default: + g_warning ("App unexpectedly in state %s", + as_app_state_to_string (state)); + g_assert_not_reached (); + } + } + + if (app_has_pending_action (self->app)) { + gtk_widget_set_visible (self->button_install, FALSE); + gtk_widget_set_visible (self->button_update, FALSE); + gtk_widget_set_visible (self->button_details_launch, FALSE); + gtk_widget_set_visible (self->button_remove, FALSE); + } +} + +static struct { + GsAppPermissions permission; + const char *title; + const char *subtitle; +} permission_display_data[] = { + { GS_APP_PERMISSIONS_NETWORK, N_("Network"), N_("Can communicate over the network") }, + { GS_APP_PERMISSIONS_SYSTEM_BUS, N_("System Services"), N_("Can access D-Bus services on the system bus") }, + { GS_APP_PERMISSIONS_SESSION_BUS, N_("Session Services"), N_("Can access D-Bus services on the session bus") }, + { GS_APP_PERMISSIONS_DEVICES, N_("Devices"), N_("Can access system device files") }, + { GS_APP_PERMISSIONS_HOME_FULL, N_("Home folder"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_HOME_READ, N_("Home folder"), N_("Can view files") }, + { GS_APP_PERMISSIONS_FILESYSTEM_FULL, N_("File system"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_FILESYSTEM_READ, N_("File system"), N_("Can view files") }, + { GS_APP_PERMISSIONS_DOWNLOADS_FULL, N_("Downloads folder"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_DOWNLOADS_READ, N_("Downloads folder"), N_("Can view files") }, + { GS_APP_PERMISSIONS_SETTINGS, N_("Settings"), N_("Can view and change any settings") }, + { GS_APP_PERMISSIONS_X11, N_("Legacy display system"), N_("Uses an old, insecure display system") }, + { GS_APP_PERMISSIONS_ESCAPE_SANDBOX, N_("Sandbox escape"), N_("Can escape the sandbox and circumvent any other restrictions") }, +}; + +static void +populate_permission_details (GsDetailsPage *self, GsAppPermissions permissions) +{ + GList *children; + + children = gtk_container_get_children (GTK_CONTAINER (self->box_permissions_details)); + for (GList *l = children; l != NULL; l = l->next) + gtk_widget_destroy (GTK_WIDGET (l->data)); + g_list_free (children); + + if (permissions == GS_APP_PERMISSIONS_NONE) { + GtkWidget *label; + label = gtk_label_new (_("This application is fully sandboxed.")); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_max_width_chars (GTK_LABEL (label), 40); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (self->box_permissions_details), label); + } else if (permissions == GS_APP_PERMISSIONS_UNKNOWN) { + GtkWidget *label; + label = gtk_label_new (_("Unable to determine which parts of the system " + "this application accesses. This is typical for " + "older applications.")); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_max_width_chars (GTK_LABEL (label), 40); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (self->box_permissions_details), label); + } else { + for (gsize i = 0; i < G_N_ELEMENTS (permission_display_data); i++) { + GtkWidget *row, *image, *box, *label; + + if ((permissions & permission_display_data[i].permission) == 0) + continue; + + row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_widget_show (row); + + image = gtk_image_new_from_icon_name ("dialog-warning-symbolic", GTK_ICON_SIZE_MENU); + if ((permission_display_data[i].permission & ~MEDIUM_PERMISSIONS) == 0) + gtk_widget_set_opacity (image, 0); + + gtk_widget_show (image); + gtk_container_add (GTK_CONTAINER (row), image); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show (box); + gtk_container_add (GTK_CONTAINER (row), box); + + label = gtk_label_new (_(permission_display_data[i].title)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (box), label); + + label = gtk_label_new (_(permission_display_data[i].subtitle)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_container_add (GTK_CONTAINER (self->box_permissions_details), row); + } + } +} + +static void +gs_details_page_refresh_all (GsDetailsPage *self) +{ + GsAppList *history; + GdkPixbuf *pixbuf = NULL; + GList *addons; + GtkWidget *widget; + const gchar *tmp; + gboolean ret; + gchar **menu_path; + guint64 kudos; + guint64 updated; + guint64 user_integration_bf; + gboolean show_support_box = FALSE; + g_autofree gchar *origin = NULL; + + /* change widgets */ + tmp = gs_app_get_name (self->app); + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header")); + if (tmp != NULL && tmp[0] != '\0') { + gtk_label_set_label (GTK_LABEL (self->application_details_title), tmp); + gtk_label_set_label (GTK_LABEL (widget), tmp); + gtk_widget_set_visible (self->application_details_title, TRUE); + } else { + gtk_widget_set_visible (self->application_details_title, FALSE); + gtk_label_set_label (GTK_LABEL (widget), ""); + } + tmp = gs_app_get_summary (self->app); + if (tmp != NULL && tmp[0] != '\0') { + gtk_label_set_label (GTK_LABEL (self->application_details_summary), tmp); + gtk_widget_set_visible (self->application_details_summary, TRUE); + } else { + gtk_widget_set_visible (self->application_details_summary, FALSE); + } + + /* refresh buttons */ + gs_details_page_refresh_buttons (self); + + /* set the description */ + tmp = gs_app_get_description (self->app); + gs_details_page_set_description (self, tmp); + + /* set the icon */ + pixbuf = gs_app_get_pixbuf (self->app); + if (pixbuf != NULL) { + gs_image_set_from_pixbuf (GTK_IMAGE (self->application_details_icon), pixbuf); + } else { + gtk_image_set_from_icon_name (GTK_IMAGE (self->application_details_icon), + "application-x-executable", + GTK_ICON_SIZE_DIALOG); + } + + tmp = gs_app_get_url (self->app, AS_URL_KIND_HOMEPAGE); + if (tmp != NULL && tmp[0] != '\0') { + gtk_widget_set_visible (self->button_details_website, TRUE); + show_support_box = TRUE; + } else { + gtk_widget_set_visible (self->button_details_website, FALSE); + } + tmp = gs_app_get_url (self->app, AS_URL_KIND_DONATION); + if (tmp != NULL && tmp[0] != '\0') { + gtk_widget_set_visible (self->button_donate, TRUE); + show_support_box = TRUE; + } else { + gtk_widget_set_visible (self->button_donate, FALSE); + } + gtk_widget_set_visible (self->box_details_support, show_support_box); + + /* set the developer name, falling back to the project group */ + tmp = gs_app_get_developer_name (self->app); + if (tmp == NULL) + tmp = gs_app_get_project_group (self->app); + if (tmp == NULL) { + gtk_widget_set_visible (self->label_details_developer_title, FALSE); + gtk_widget_set_visible (self->box_details_developer, FALSE); + } else { + gtk_widget_set_visible (self->label_details_developer_title, TRUE); + gtk_label_set_label (GTK_LABEL (self->label_details_developer_value), tmp); + gtk_widget_set_visible (self->box_details_developer, TRUE); + } + gtk_widget_set_visible (self->image_details_developer_verified, gs_app_has_quirk (self->app, GS_APP_QUIRK_DEVELOPER_VERIFIED)); + + /* set the license buttons */ + tmp = gs_app_get_license (self->app); + if (tmp == NULL) { + gtk_widget_set_visible (self->button_details_license_free, FALSE); + gtk_widget_set_visible (self->button_details_license_nonfree, FALSE); + gtk_widget_set_visible (self->button_details_license_unknown, TRUE); + } else if (gs_app_get_license_is_free (self->app)) { + gtk_widget_set_visible (self->button_details_license_free, TRUE); + gtk_widget_set_visible (self->button_details_license_nonfree, FALSE); + gtk_widget_set_visible (self->button_details_license_unknown, FALSE); + } else { + gtk_widget_set_visible (self->button_details_license_free, FALSE); + gtk_widget_set_visible (self->button_details_license_nonfree, TRUE); + gtk_widget_set_visible (self->button_details_license_unknown, FALSE); + } + + /* set channel for snaps */ + if (gs_app_get_bundle_kind (self->app) == AS_BUNDLE_KIND_SNAP) { + gtk_label_set_label (GTK_LABEL (self->label_details_channel_value), gs_app_get_branch (self->app)); + gtk_widget_set_visible (self->label_details_channel_title, TRUE); + gtk_widget_set_visible (self->label_details_channel_value, TRUE); + } else { + gtk_widget_set_visible (self->label_details_channel_title, FALSE); + gtk_widget_set_visible (self->label_details_channel_value, FALSE); + } + + /* set version */ + tmp = gs_app_get_version (self->app); + if (tmp != NULL){ + gtk_label_set_label (GTK_LABEL (self->label_details_version_value), tmp); + } else { + /* TRANSLATORS: this is where the version is not known */ + gtk_label_set_label (GTK_LABEL (self->label_details_version_value), C_("version", "Unknown")); + } + + /* refresh size information */ + gs_details_page_refresh_size (self); + + /* set the updated date */ + updated = gs_app_get_install_date (self->app); + if (updated == GS_APP_INSTALL_DATE_UNSET) { + gtk_widget_set_visible (self->label_details_updated_title, FALSE); + gtk_widget_set_visible (self->label_details_updated_value, FALSE); + } else if (updated == GS_APP_INSTALL_DATE_UNKNOWN) { + /* TRANSLATORS: this is where the updated date is not known */ + gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), C_("updated", "Never")); + gtk_widget_set_visible (self->label_details_updated_title, TRUE); + gtk_widget_set_visible (self->label_details_updated_value, TRUE); + } else { + g_autoptr(GDateTime) dt = NULL; + g_autofree gchar *updated_str = NULL; + dt = g_date_time_new_from_unix_utc ((gint64) updated); + updated_str = g_date_time_format (dt, "%x"); + + history = gs_app_get_history (self->app); + + if (gs_app_list_length (history) == 0) { + gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), updated_str); + } else { + GString *url; + + url = g_string_new (NULL); + g_string_printf (url, "<a href=\"show-history\">%s</a>", updated_str); + gtk_label_set_markup (GTK_LABEL (self->label_details_updated_value), url->str); + g_string_free (url, TRUE); + } + gtk_widget_set_visible (self->label_details_updated_title, TRUE); + gtk_widget_set_visible (self->label_details_updated_value, TRUE); + } + + /* set the category */ + menu_path = gs_app_get_menu_path (self->app); + if (menu_path == NULL || menu_path[0] == NULL || menu_path[0][0] == '\0') { + gtk_widget_set_visible (self->label_details_category_title, FALSE); + gtk_widget_set_visible (self->label_details_category_value, FALSE); + } else { + g_autofree gchar *path = NULL; + if (gtk_widget_get_direction (self->label_details_category_value) == GTK_TEXT_DIR_RTL) + path = g_strjoinv (" ← ", menu_path); + else + path = g_strjoinv (" → ", menu_path); + gtk_label_set_label (GTK_LABEL (self->label_details_category_value), path); + gtk_widget_set_visible (self->label_details_category_title, TRUE); + gtk_widget_set_visible (self->label_details_category_value, TRUE); + } + + /* set the origin */ + origin = g_strdup (gs_app_get_origin_hostname (self->app)); + if (origin == NULL) + origin = g_strdup (gs_app_get_origin (self->app)); + if (origin == NULL) { + GFile *local_file = gs_app_get_local_file (self->app); + if (local_file != NULL) + origin = g_file_get_basename (local_file); + } + if (origin == NULL || origin[0] == '\0') { + /* TRANSLATORS: this is where we don't know the origin of the + * application */ + gtk_label_set_label (GTK_LABEL (self->label_details_origin_value), C_("origin", "Unknown")); + } else { + gtk_label_set_label (GTK_LABEL (self->label_details_origin_value), origin); + } + + /* set MyLanguage kudo */ + kudos = gs_app_get_kudos (self->app); + ret = (kudos & GS_APP_KUDO_MY_LANGUAGE) > 0; + gtk_widget_set_sensitive (self->image_details_kudo_translated, ret); + gs_details_page_set_sensitive (self->label_details_kudo_translated, ret); + + /* set RecentRelease kudo */ + ret = (kudos & GS_APP_KUDO_RECENT_RELEASE) > 0; + gtk_widget_set_sensitive (self->image_details_kudo_updated, ret); + gs_details_page_set_sensitive (self->label_details_kudo_updated, ret); + + /* set UserDocs kudo */ + ret = (kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0; + gtk_widget_set_sensitive (self->image_details_kudo_docs, ret); + gs_details_page_set_sensitive (self->label_details_kudo_docs, ret); + + /* set sandboxed kudo */ + ret = (kudos & GS_APP_KUDO_SANDBOXED) > 0; + gtk_widget_set_sensitive (self->image_details_kudo_sandboxed, ret); + gs_details_page_set_sensitive (self->label_details_kudo_sandboxed, ret); + + /* any of the various integration kudos */ + user_integration_bf = GS_APP_KUDO_SEARCH_PROVIDER | + GS_APP_KUDO_USES_NOTIFICATIONS | + GS_APP_KUDO_HIGH_CONTRAST; + ret = (kudos & user_integration_bf) > 0; + gtk_widget_set_sensitive (self->image_details_kudo_integration, ret); + gs_details_page_set_sensitive (self->label_details_kudo_integration, ret); + + /* hide the kudo details for non-desktop software */ + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_DESKTOP: + gtk_widget_set_visible (self->grid_details_kudo, TRUE); + break; + default: + gtk_widget_set_visible (self->grid_details_kudo, FALSE); + break; + } + + /* only show permissions for flatpak apps */ + if (gs_app_get_bundle_kind (self->app) == AS_BUNDLE_KIND_FLATPAK && + gs_app_get_kind (self->app) == AS_APP_KIND_DESKTOP) { + GsAppPermissions permissions = gs_app_get_permissions (self->app); + + populate_permission_details (self, permissions); + + if (gs_app_get_permissions (self->app) != GS_APP_PERMISSIONS_UNKNOWN) { + if ((permissions & ~LIMITED_PERMISSIONS) == 0) + gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Low")); + else if ((permissions & ~MEDIUM_PERMISSIONS) == 0) + gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Medium")); + else + gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("High")); + } else { + gtk_button_set_label (GTK_BUTTON (self->button_details_permissions_value), _("Unknown")); + } + + gtk_widget_set_visible (self->label_details_permissions_title, TRUE); + gtk_widget_set_visible (self->button_details_permissions_value, TRUE); + } else { + gtk_widget_set_visible (self->label_details_permissions_title, FALSE); + gtk_widget_set_visible (self->button_details_permissions_value, FALSE); + } + + /* are we trying to replace something in the baseos */ + gtk_widget_set_visible (self->infobar_details_package_baseos, + gs_app_has_quirk (self->app, GS_APP_QUIRK_COMPULSORY) && + gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL); + + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_DESKTOP: + /* installing an app with a repo file */ + gtk_widget_set_visible (self->infobar_details_app_repo, + gs_app_has_quirk (self->app, + GS_APP_QUIRK_HAS_SOURCE) && + gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL); + gtk_widget_set_visible (self->infobar_details_repo, FALSE); + break; + case AS_APP_KIND_GENERIC: + /* installing a repo-release package */ + gtk_widget_set_visible (self->infobar_details_app_repo, FALSE); + gtk_widget_set_visible (self->infobar_details_repo, + gs_app_has_quirk (self->app, + GS_APP_QUIRK_HAS_SOURCE) && + gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL); + break; + default: + gtk_widget_set_visible (self->infobar_details_app_repo, FALSE); + gtk_widget_set_visible (self->infobar_details_repo, FALSE); + break; + } + + /* installing a app without a repo file */ + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_DESKTOP: + if (gs_app_get_kind (self->app) == AS_APP_KIND_FIRMWARE) { + gtk_widget_set_visible (self->infobar_details_app_norepo, FALSE); + } else { + gtk_widget_set_visible (self->infobar_details_app_norepo, + !gs_app_has_quirk (self->app, + GS_APP_QUIRK_HAS_SOURCE) && + gs_app_get_state (self->app) == AS_APP_STATE_AVAILABLE_LOCAL); + } + break; + default: + gtk_widget_set_visible (self->infobar_details_app_norepo, FALSE); + break; + } + + /* only show the "select addons" string if the app isn't yet installed */ + switch (gs_app_get_state (self->app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + gtk_widget_set_visible (self->label_addons_uninstalled_app, FALSE); + break; + default: + gtk_widget_set_visible (self->label_addons_uninstalled_app, TRUE); + break; + } + + /* hide fields that don't make sense for sources */ + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_SOURCE: + gtk_widget_set_visible (self->label_details_license_title, FALSE); + gtk_widget_set_visible (self->box_details_license_value, FALSE); + gtk_widget_set_visible (self->label_details_version_title, FALSE); + gtk_widget_set_visible (self->label_details_version_value, FALSE); + break; + default: + gtk_widget_set_visible (self->label_details_license_title, TRUE); + gtk_widget_set_visible (self->box_details_license_value, TRUE); + gtk_widget_set_visible (self->label_details_version_title, TRUE); + gtk_widget_set_visible (self->label_details_version_value, TRUE); + break; + } + + gs_details_page_update_shortcut_button (self); + + /* update progress */ + gs_details_page_refresh_progress (self); + + addons = gtk_container_get_children (GTK_CONTAINER (self->list_box_addons)); + gtk_widget_set_visible (self->box_addons, addons != NULL); + g_list_free (addons); +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header = NULL; + if (before != NULL) + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static gint +list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GsApp *a1 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (a)); + GsApp *a2 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (b)); + + return gs_utils_sort_strcmp (gs_app_get_name (a1), + gs_app_get_name (a2)); +} + +static void gs_details_page_addon_selected_cb (GsAppAddonRow *row, GParamSpec *pspec, GsDetailsPage *self); + +static void +gs_details_page_refresh_addons (GsDetailsPage *self) +{ + GsAppList *addons; + guint i; + + gs_container_remove_all (GTK_CONTAINER (self->list_box_addons)); + + addons = gs_app_get_addons (self->app); + for (i = 0; i < gs_app_list_length (addons); i++) { + GsApp *addon; + GtkWidget *row; + + addon = gs_app_list_index (addons, i); + if (gs_app_get_state (addon) == AS_APP_STATE_UNKNOWN || + gs_app_get_state (addon) == AS_APP_STATE_UNAVAILABLE) + continue; + + row = gs_app_addon_row_new (addon); + + gtk_container_add (GTK_CONTAINER (self->list_box_addons), row); + gtk_widget_show (row); + + g_signal_connect (row, "notify::selected", + G_CALLBACK (gs_details_page_addon_selected_cb), + self); + } +} + +static void gs_details_page_refresh_reviews (GsDetailsPage *self); + +typedef struct { + GsDetailsPage *self; + AsReview *review; + GsApp *app; + GsPluginAction action; +} GsDetailsPageReviewHelper; + +static void +gs_details_page_review_helper_free (GsDetailsPageReviewHelper *helper) +{ + g_object_unref (helper->self); + g_object_unref (helper->review); + g_object_unref (helper->app); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDetailsPageReviewHelper, gs_details_page_review_helper_free); + +static void +gs_details_page_app_set_review_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GsDetailsPageReviewHelper) helper = (GsDetailsPageReviewHelper *) user_data; + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to set review on %s: %s", + gs_app_get_id (helper->app), error->message); + return; + } + gs_details_page_refresh_reviews (helper->self); +} + +static void +gs_details_page_review_button_clicked_cb (GsReviewRow *row, + GsPluginAction action, + GsDetailsPage *self) +{ + GsDetailsPageReviewHelper *helper = g_new0 (GsDetailsPageReviewHelper, 1); + g_autoptr(GsPluginJob) plugin_job = NULL; + + helper->self = g_object_ref (self); + helper->app = g_object_ref (self->app); + helper->review = g_object_ref (gs_review_row_get_review (row)); + helper->action = action; + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "app", helper->app, + "review", helper->review, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_app_set_review_cb, + helper); +} + +static void +gs_details_page_refresh_reviews (GsDetailsPage *self) +{ + GArray *review_ratings = NULL; + GPtrArray *reviews; + gboolean show_review_button = TRUE; + gboolean show_reviews = FALSE; + guint n_reviews = 0; + guint64 possible_actions = 0; + guint i; + struct { + GsPluginAction action; + const gchar *plugin_func; + } plugin_vfuncs[] = { + { GS_PLUGIN_ACTION_REVIEW_UPVOTE, "gs_plugin_review_upvote" }, + { GS_PLUGIN_ACTION_REVIEW_DOWNVOTE, "gs_plugin_review_downvote" }, + { GS_PLUGIN_ACTION_REVIEW_REPORT, "gs_plugin_review_report" }, + { GS_PLUGIN_ACTION_REVIEW_SUBMIT, "gs_plugin_review_submit" }, + { GS_PLUGIN_ACTION_REVIEW_REMOVE, "gs_plugin_review_remove" }, + { GS_PLUGIN_ACTION_LAST, NULL } + }; + + /* nothing to show */ + if (self->app == NULL) + return; + + /* show or hide the entire reviews section */ + switch (gs_app_get_kind (self->app)) { + case AS_APP_KIND_DESKTOP: + case AS_APP_KIND_FONT: + case AS_APP_KIND_INPUT_METHOD: + case AS_APP_KIND_WEB_APP: + case AS_APP_KIND_SHELL_EXTENSION: + /* don't show a missing rating on a local file */ + if (gs_app_get_state (self->app) != AS_APP_STATE_AVAILABLE_LOCAL && + self->enable_reviews) + show_reviews = TRUE; + break; + default: + break; + } + + /* some apps are unreviewable */ + if (gs_app_has_quirk (self->app, GS_APP_QUIRK_NOT_REVIEWABLE)) + show_reviews = FALSE; + + /* set the star rating */ + if (show_reviews) { + gtk_widget_set_sensitive (self->star, gs_app_get_rating (self->app) >= 0); + gs_star_widget_set_rating (GS_STAR_WIDGET (self->star), + gs_app_get_rating (self->app)); + + review_ratings = gs_app_get_review_ratings (self->app); + if (review_ratings != NULL) { + gs_review_histogram_set_ratings (GS_REVIEW_HISTOGRAM (self->histogram), + review_ratings); + } + if (review_ratings != NULL) { + for (i = 0; i < review_ratings->len; i++) + n_reviews += (guint) g_array_index (review_ratings, guint32, i); + } else if (gs_app_get_reviews (self->app) != NULL) { + n_reviews = gs_app_get_reviews (self->app)->len; + } + } + + /* enable appropriate widgets */ + gtk_widget_set_visible (self->star, show_reviews); + gtk_widget_set_visible (self->box_reviews, show_reviews); + gtk_widget_set_visible (self->histogram, review_ratings != NULL && review_ratings->len > 0); + gtk_widget_set_visible (self->label_review_count, n_reviews > 0); + + /* update the review label next to the star widget */ + if (n_reviews > 0) { + g_autofree gchar *text = NULL; + gtk_widget_set_visible (self->label_review_count, TRUE); + text = g_strdup_printf ("(%u)", n_reviews); + gtk_label_set_text (GTK_LABEL (self->label_review_count), text); + } + + /* no point continuing */ + if (!show_reviews) + return; + + /* find what the plugins support */ + for (i = 0; plugin_vfuncs[i].action != GS_PLUGIN_ACTION_LAST; i++) { + if (gs_plugin_loader_get_plugin_supported (self->plugin_loader, + plugin_vfuncs[i].plugin_func)) { + possible_actions |= 1u << plugin_vfuncs[i].action; + } + } + + /* add all the reviews */ + gs_container_remove_all (GTK_CONTAINER (self->list_box_reviews)); + reviews = gs_app_get_reviews (self->app); + for (i = 0; i < reviews->len; i++) { + AsReview *review = g_ptr_array_index (reviews, i); + GtkWidget *row = gs_review_row_new (review); + guint64 actions; + + g_signal_connect (row, "button-clicked", + G_CALLBACK (gs_details_page_review_button_clicked_cb), self); + if (as_review_get_flags (review) & AS_REVIEW_FLAG_SELF) { + actions = possible_actions & 1 << GS_PLUGIN_ACTION_REVIEW_REMOVE; + show_review_button = FALSE; + } else { + actions = possible_actions & ~(1u << GS_PLUGIN_ACTION_REVIEW_REMOVE); + } + gs_review_row_set_actions (GS_REVIEW_ROW (row), actions); + gtk_container_add (GTK_CONTAINER (self->list_box_reviews), row); + gtk_widget_set_visible (row, self->show_all_reviews || + i < SHOW_NR_REVIEWS_INITIAL); + gs_review_row_set_network_available (GS_REVIEW_ROW (row), + gs_plugin_loader_get_network_available (self->plugin_loader)); + } + + /* only show the button if there are more to show */ + gtk_widget_set_visible (self->button_more_reviews, + !self->show_all_reviews && + reviews->len > SHOW_NR_REVIEWS_INITIAL); + + /* show the button only if the user never reviewed */ + gtk_widget_set_visible (self->button_review, show_review_button); + if (gs_plugin_loader_get_network_available (self->plugin_loader)) { + gtk_widget_set_sensitive (self->button_review, TRUE); + gtk_widget_set_sensitive (self->star_eventbox, TRUE); + gtk_widget_set_tooltip_text (self->button_review, NULL); + } else { + gtk_widget_set_sensitive (self->button_review, FALSE); + gtk_widget_set_sensitive (self->star_eventbox, FALSE); + gtk_widget_set_tooltip_text (self->button_review, + /* TRANSLATORS: we need a remote server to process */ + _("You need internet access to write a review")); + } +} + +static void +gs_details_page_app_refine_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to refine %s: %s", + gs_app_get_id (self->app), + error->message); + return; + } + gs_details_page_refresh_size (self); + gs_details_page_refresh_reviews (self); +} + +static void +gs_details_page_content_rating_set_css (GsDetailsPage *page, guint age) +{ + g_autoptr(GString) css = g_string_new (NULL); + const gchar *color_bg = NULL; + const gchar *color_fg = "#ffffff"; + if (age >= 18) { + color_bg = "#ee2222"; + } else if (age >= 15) { + color_bg = "#f1c000"; + } else if (age >= 12) { + color_bg = "#2a97c9"; + } else if (age >= 5) { + color_bg = "#3f756c"; + } else { + color_bg = "#009d66"; + } + g_string_append_printf (css, "color: %s;\n", color_fg); + g_string_append_printf (css, "background-color: %s;\n", color_bg); + + gs_utils_widget_set_css (page->button_details_rating_value, + (GtkCssProvider **) &page->button_details_rating_style_provider, + "content-rating-custom", css->str); +} + +static void +gs_details_page_refresh_content_rating (GsDetailsPage *self) +{ + AsContentRating *content_rating; + GsContentRatingSystem system; + guint age = 0; + g_autofree gchar *display = NULL; + const gchar *locale; + + /* get the content rating system from the locale */ + locale = setlocale (LC_MESSAGES, NULL); + system = gs_utils_content_rating_system_from_locale (locale); + g_debug ("content rating system is guessed as %s from %s", + gs_content_rating_system_to_str (system), + locale); + + /* only show the button if a game and has a content rating */ + content_rating = gs_app_get_content_rating (self->app); + if (content_rating != NULL) { + age = as_content_rating_get_minimum_age (content_rating); + display = gs_utils_content_rating_age_to_str (system, age); + } + if (display != NULL) { + gtk_button_set_label (GTK_BUTTON (self->button_details_rating_value), display); + gtk_widget_set_visible (self->button_details_rating_value, TRUE); + gtk_widget_set_visible (self->label_details_rating_title, TRUE); + gs_details_page_content_rating_set_css (self, age); + } else { + gtk_widget_set_visible (self->button_details_rating_value, FALSE); + gtk_widget_set_visible (self->label_details_rating_title, FALSE); + } +} + +static void +_set_app (GsDetailsPage *self, GsApp *app) +{ + /* do not show all the reviews by default */ + self->show_all_reviews = FALSE; + + /* disconnect the old handlers */ + if (self->app != NULL) { + g_signal_handlers_disconnect_by_func (self->app, gs_details_page_notify_state_changed_cb, self); + g_signal_handlers_disconnect_by_func (self->app, gs_details_page_progress_changed_cb, self); + g_signal_handlers_disconnect_by_func (self->app, gs_details_page_allow_cancel_changed_cb, + self); + } + + /* save app */ + g_set_object (&self->app, app); + if (self->app == NULL) { + /* switch away from the details view that failed to load */ + gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW); + return; + } + g_set_object (&self->app_cancellable, gs_app_get_cancellable (app)); + g_signal_connect_object (self->app, "notify::state", + G_CALLBACK (gs_details_page_notify_state_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::size", + G_CALLBACK (gs_details_page_notify_state_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::license", + G_CALLBACK (gs_details_page_notify_state_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::quirk", + G_CALLBACK (gs_details_page_notify_state_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::progress", + G_CALLBACK (gs_details_page_progress_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::allow-cancel", + G_CALLBACK (gs_details_page_allow_cancel_changed_cb), + self, 0); + g_signal_connect_object (self->app, "notify::pending-action", + G_CALLBACK (gs_details_page_notify_state_changed_cb), + self, 0); +} + +/* show the UI and do operations that should not block page load */ +static void +gs_details_page_load_stage2 (GsDetailsPage *self) +{ + g_autofree gchar *tmp = NULL; + g_autoptr(GsPluginJob) plugin_job1 = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + + /* print what we've got */ + tmp = gs_app_to_string (self->app); + g_debug ("%s", tmp); + + /* update UI */ + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_READY); + gs_details_page_refresh_screenshots (self); + gs_details_page_refresh_addons (self); + gs_details_page_refresh_reviews (self); + gs_details_page_refresh_all (self); + gs_details_page_refresh_content_rating (self); + + /* if these tasks fail (e.g. because we have no networking) then it's + * of no huge importance if we don't get the required data */ + plugin_job1 = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", self->app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE, + NULL); + plugin_job2 = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_ALTERNATES, + "interactive", TRUE, + "app", self->app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job1, + self->cancellable, + gs_details_page_app_refine_cb, + self); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job2, + self->cancellable, + gs_details_page_get_alternates_cb, + self); +} + +static void +gs_details_page_load_stage1_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to refine %s: %s", + gs_app_get_id (self->app), + error->message); + } + if (gs_app_get_kind (self->app) == AS_APP_KIND_UNKNOWN || + gs_app_get_state (self->app) == AS_APP_STATE_UNKNOWN) { + g_autofree gchar *str = NULL; + const gchar *id = gs_app_get_id (self->app); + str = g_strdup_printf (_("Unable to find “%s”"), id == NULL ? gs_app_get_source_default (self->app) : id); + gtk_label_set_text (GTK_LABEL (self->label_failed), str); + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_FAILED); + return; + } + + /* Hide the app if it’s not suitable for the user, but only if it’s not + * already installed — a parent could have decided that a particular + * app *is* actually suitable for their child, despite its age rating. + * + * Make it look like the app doesn’t exist, to not tantalise the + * child. */ + if (!gs_app_is_installed (self->app) && + gs_app_has_quirk (self->app, GS_APP_QUIRK_PARENTAL_FILTER)) { + g_autofree gchar *str = NULL; + const gchar *id = gs_app_get_id (self->app); + str = g_strdup_printf (_("Unable to find “%s”"), id == NULL ? gs_app_get_source_default (self->app) : id); + gtk_label_set_text (GTK_LABEL (self->label_failed), str); + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_FAILED); + return; + } + + /* do 2nd stage refine */ + gs_details_page_load_stage2 (self); +} + +static void +gs_details_page_file_to_app_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) error = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + g_warning ("failed to convert file to GsApp: %s", error->message); + /* go back to the overview */ + gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW); + } else { + GsApp *app = gs_app_list_index (list, 0); + g_set_object (&self->app_local_file, app); + _set_app (self, app); + gs_details_page_load_stage2 (self); + } +} + +static void +gs_details_page_url_to_app_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) error = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + g_warning ("failed to convert URL to GsApp: %s", error->message); + /* go back to the overview */ + gs_shell_set_mode (self->shell, GS_SHELL_MODE_OVERVIEW); + } else { + GsApp *app = gs_app_list_index (list, 0); + _set_app (self, app); + gs_details_page_load_stage2 (self); + } +} + +void +gs_details_page_set_local_file (GsDetailsPage *self, GFile *file) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING); + g_clear_object (&self->app_local_file); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_file_to_app_cb, + self); +} + +void +gs_details_page_set_url (GsDetailsPage *self, const gchar *url) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING); + g_clear_object (&self->app_local_file); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", url, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_url_to_app_cb, + self); +} + +/* refines a GsApp */ +static void +gs_details_page_load_stage1 (GsDetailsPage *self) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* update UI */ + gs_page_switch_to (GS_PAGE (self), TRUE); + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING); + + /* get extra details about the app */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", self->app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_load_stage1_cb, + self); + + /* update UI with loading page */ + gs_details_page_refresh_all (self); +} + +static void +gs_details_page_reload (GsPage *page) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (page); + if (self->app != NULL) + gs_details_page_load_stage1 (self); +} + +static gint +origin_popover_list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GsApp *a1 = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (a)); + GsApp *a2 = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (b)); + g_autofree gchar *a1_origin = gs_app_get_origin_ui (a1); + g_autofree gchar *a2_origin = gs_app_get_origin_ui (a2); + + return gs_utils_sort_strcmp (a1_origin, a2_origin); +} + +static void +origin_popover_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + gpointer user_data) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (user_data); + GsApp *app; + GtkWidget *popover; + + popover = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover")); + gtk_popover_popdown (GTK_POPOVER (popover)); + + app = gs_origin_popover_row_get_app (GS_ORIGIN_POPOVER_ROW (row)); + if (app != self->app) { + _set_app (self, app); + gs_details_page_load_stage1 (self); + } +} + +static void +settings_changed_cb (GsDetailsPage *self, const gchar *key, gpointer data) +{ + if (self->app == NULL) + return; + if (g_strcmp0 (key, "show-nonfree-ui") == 0) { + gs_details_page_refresh_all (self); + } +} + +/* this is being called from GsShell */ +void +gs_details_page_set_app (GsDetailsPage *self, GsApp *app) +{ + g_return_if_fail (GS_IS_DETAILS_PAGE (self)); + g_return_if_fail (GS_IS_APP (app)); + + /* clear old state */ + g_clear_object (&self->app_local_file); + + /* save GsApp */ + _set_app (self, app); + gs_details_page_load_stage1 (self); +} + +GsApp * +gs_details_page_get_app (GsDetailsPage *self) +{ + return self->app; +} + +static void +gs_details_page_remove_app (GsDetailsPage *self) +{ + g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app)); + gs_page_remove_app (GS_PAGE (self), self->app, self->app_cancellable); +} + +static void +gs_details_page_app_remove_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + gs_details_page_remove_app (self); +} + +static void +gs_details_page_app_cancel_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + g_cancellable_cancel (self->app_cancellable); + gtk_widget_set_sensitive (widget, FALSE); + + /* reset the pending-action from the app if needed */ + gs_app_set_pending_action (self->app, GS_PLUGIN_ACTION_UNKNOWN); + + /* FIXME: We should be able to revert the QUEUED_FOR_INSTALL without + * having to pretend to remove the app */ + if (gs_app_get_state (self->app) == AS_APP_STATE_QUEUED_FOR_INSTALL) + gs_details_page_remove_app (self); +} + +static void +gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + g_autoptr(GList) addons = NULL; + + /* Mark ticked addons to be installed together with the app */ + addons = gtk_container_get_children (GTK_CONTAINER (self->list_box_addons)); + for (GList *l = addons; l; l = l->next) { + if (gs_app_addon_row_get_selected (l->data)) { + GsApp *addon = gs_app_addon_row_get_addon (l->data); + + if (gs_app_get_state (addon) == AS_APP_STATE_AVAILABLE) + gs_app_set_to_be_installed (addon, TRUE); + } + } + + g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app)); + + if (gs_app_get_state (self->app) == AS_APP_STATE_UPDATABLE_LIVE) { + gs_page_update_app (GS_PAGE (self), self->app, self->app_cancellable); + return; + } + + gs_page_install_app (GS_PAGE (self), self->app, GS_SHELL_INTERACTION_FULL, + self->app_cancellable); +} + +static void +gs_details_page_app_update_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + g_set_object (&self->app_cancellable, gs_app_get_cancellable (self->app)); + gs_page_update_app (GS_PAGE (self), self->app, self->app_cancellable); +} + +static void +gs_details_page_addon_selected_cb (GsAppAddonRow *row, + GParamSpec *pspec, + GsDetailsPage *self) +{ + GsApp *addon; + + addon = gs_app_addon_row_get_addon (row); + + /* If the main app is already installed, ticking the addon checkbox + * triggers an immediate install. Otherwise we'll install the addon + * together with the main app. */ + switch (gs_app_get_state (self->app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + if (gs_app_addon_row_get_selected (row)) { + g_set_object (&self->app_cancellable, gs_app_get_cancellable (addon)); + gs_page_install_app (GS_PAGE (self), addon, GS_SHELL_INTERACTION_FULL, + self->app_cancellable); + } else { + g_set_object (&self->app_cancellable, gs_app_get_cancellable (addon)); + gs_page_remove_app (GS_PAGE (self), addon, self->app_cancellable); + /* make sure the addon checkboxes are synced if the + * user clicks cancel in the remove confirmation dialog */ + gs_details_page_refresh_addons (self); + gs_details_page_refresh_all (self); + } + break; + default: + break; + } +} + +static void +gs_details_page_app_launch_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + + /* hide the notification */ + g_application_withdraw_notification (g_application_get_default (), + "installed"); + + g_set_object (&self->cancellable, cancellable); + gs_page_launch_app (GS_PAGE (self), self->app, self->cancellable); +} + +static void +gs_details_page_app_add_shortcut_button_cb (GtkWidget *widget, + GsDetailsPage *self) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + g_set_object (&self->cancellable, cancellable); + gs_page_shortcut_add (GS_PAGE (self), self->app, self->cancellable); +} + +static void +gs_details_page_app_remove_shortcut_button_cb (GtkWidget *widget, + GsDetailsPage *self) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + g_set_object (&self->cancellable, cancellable); + gs_page_shortcut_remove (GS_PAGE (self), self->app, self->cancellable); +} + +static void +gs_details_page_review_response_cb (GtkDialog *dialog, + gint response, + GsDetailsPage *self) +{ + g_autofree gchar *text = NULL; + g_autoptr(GDateTime) now = NULL; + g_autoptr(AsReview) review = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsDetailsPageReviewHelper *helper; + GsReviewDialog *rdialog = GS_REVIEW_DIALOG (dialog); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) { + gtk_widget_destroy (GTK_WIDGET (dialog)); + return; + } + + review = as_review_new (); + as_review_set_summary (review, gs_review_dialog_get_summary (rdialog)); + text = gs_review_dialog_get_text (rdialog); + as_review_set_description (review, text); + as_review_set_rating (review, gs_review_dialog_get_rating (rdialog)); + as_review_set_version (review, gs_app_get_version (self->app)); + now = g_date_time_new_now_local (); + as_review_set_date (review, now); + + /* call into the plugins to set the new value */ + helper = g_new0 (GsDetailsPageReviewHelper, 1); + helper->self = g_object_ref (self); + helper->app = g_object_ref (self->app); + helper->review = g_object_ref (review); + helper->action = GS_PLUGIN_ACTION_REVIEW_SUBMIT; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REVIEW_SUBMIT, + "interactive", TRUE, + "app", helper->app, + "review", helper->review, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_app_set_review_cb, + helper); + + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +gs_details_page_write_review_cb (GtkButton *button, + GsDetailsPage *self) +{ + GtkWidget *dialog; + dialog = gs_review_dialog_new (); + g_signal_connect (dialog, "response", + G_CALLBACK (gs_details_page_review_response_cb), self); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); +} + +static void +gs_details_page_app_installed (GsPage *page, GsApp *app) +{ + gs_details_page_reload (page); +} + +static void +gs_details_page_app_removed (GsPage *page, GsApp *app) +{ + gs_details_page_reload (page); +} + +static void +show_all_cb (GtkWidget *widget, gpointer user_data) +{ + gtk_widget_show (widget); +} + +static void +gs_details_page_more_reviews_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + self->show_all_reviews = TRUE; + gtk_container_foreach (GTK_CONTAINER (self->list_box_reviews), + show_all_cb, NULL); + gtk_widget_set_visible (self->button_more_reviews, FALSE); +} + +static void +gs_details_page_content_rating_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + AsContentRating *cr; + AsContentRatingValue value_bad = AS_CONTENT_RATING_VALUE_NONE; + const gchar *tmp; + g_autofree const gchar **ids = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + /* Ordered from worst to best */ + const gchar *violence_group[] = { + "violence-bloodshed", + "violence-realistic", + "violence-fantasy", + "violence-cartoon", + NULL + }; + const gchar *social_group[] = { + "social-audio", + "social-chat", + "social-contacts", + "social-info", + NULL + }; + + cr = gs_app_get_content_rating (self->app); + if (cr == NULL) + return; + + ids = gs_content_rating_get_all_rating_ids (); + + /* get the worst thing */ + for (gsize i = 0; ids[i] != NULL; i++) { + AsContentRatingValue value; + value = as_content_rating_get_value (cr, ids[i]); + if (value > value_bad) + value_bad = value; + } + + /* get the content rating description for the worst things about the app; + * handle the groups separately*/ + for (gsize i = 0; ids[i] != NULL; i++) { + if (!g_strv_contains (violence_group, ids[i]) && + !g_strv_contains (social_group, ids[i])) { + AsContentRatingValue value; + value = as_content_rating_get_value (cr, ids[i]); + if (value < value_bad) + continue; + tmp = gs_content_rating_key_value_to_str (ids[i], value); + g_string_append_printf (str, "• %s\n", tmp); + } + } + + for (gsize i = 0; violence_group[i] != NULL; i++) { + AsContentRatingValue value; + value = as_content_rating_get_value (cr, violence_group[i]); + if (value < value_bad) + continue; + tmp = gs_content_rating_key_value_to_str (violence_group[i], value); + g_string_append_printf (str, "• %s\n", tmp); + break; + } + + for (gsize i = 0; social_group[i] != NULL; i++) { + AsContentRatingValue value; + value = as_content_rating_get_value (cr, social_group[i]); + if (value < value_bad) + continue; + tmp = gs_content_rating_key_value_to_str (social_group[i], value); + g_string_append_printf (str, "• %s\n", tmp); + break; + } + + if (str->len > 0) + g_string_truncate (str, str->len - 1); + + /* enable the details if there are any */ + gtk_label_set_label (GTK_LABEL (self->label_content_rating_message), str->str); + gtk_widget_set_visible (self->label_content_rating_title, str->len > 0); + gtk_widget_set_visible (self->label_content_rating_message, str->len > 0); + gtk_widget_set_visible (self->label_content_rating_none, str->len == 0); + + /* show popover */ + gtk_popover_set_relative_to (GTK_POPOVER (self->popover_content_rating), widget); + gtk_widget_show (self->popover_content_rating); +} + +static void +gs_details_page_permissions_button_cb (GtkWidget *widget, GsDetailsPage *self) +{ + gtk_widget_show (self->popover_permissions); +} + +static gboolean +gs_details_page_activate_link_cb (GtkLabel *label, + const gchar *uri, + GsDetailsPage *self) +{ + gs_shell_show_uri (self->shell, uri); + return TRUE; +} + +static GtkWidget * +gs_details_page_label_widget (GsDetailsPage *self, + const gchar *title, + const gchar *url) +{ + GtkWidget *w; + g_autofree gchar *markup = NULL; + + markup = g_strdup_printf ("<a href=\"%s\">%s</a>", url, title); + w = gtk_label_new (markup); + g_signal_connect (w, "activate-link", + G_CALLBACK (gs_details_page_activate_link_cb), + self); + gtk_label_set_use_markup (GTK_LABEL (w), TRUE); + gtk_label_set_xalign (GTK_LABEL (w), 0.f); + gtk_widget_set_visible (w, TRUE); + return w; +} + +static GtkWidget * +gs_details_page_license_widget_for_token (GsDetailsPage *self, const gchar *token) +{ + /* public domain */ + if (g_strcmp0 (token, "@LicenseRef-public-domain") == 0) { + /* TRANSLATORS: see the wikipedia page */ + return gs_details_page_label_widget (self, _("Public domain"), + /* TRANSLATORS: Replace the link with a version in your language, + * e.g. https://de.wikipedia.org/wiki/Gemeinfreiheit */ + _("https://en.wikipedia.org/wiki/Public_domain")); + } + + /* free software, license unspecified */ + if (g_str_has_prefix (token, "@LicenseRef-free")) { + /* TRANSLATORS: Replace the link with a version in your language, + * e.g. https://www.gnu.org/philosophy/free-sw.de */ + const gchar *url = _("https://www.gnu.org/philosophy/free-sw"); + gchar *tmp; + + /* we support putting a custom URL in the + * token string, e.g. @LicenseRef-free=http://ubuntu.com */ + tmp = g_strstr_len (token, -1, "="); + if (tmp != NULL) + url = tmp + 1; + + /* TRANSLATORS: see GNU page */ + return gs_details_page_label_widget (self, _("Free Software"), url); + } + + /* SPDX value */ + if (g_str_has_prefix (token, "@")) { + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("http://spdx.org/licenses/%s", + token + 1); + return gs_details_page_label_widget (self, token + 1, uri); + } + + /* new SPDX value the extractor didn't know about */ + if (as_utils_is_spdx_license_id (token)) { + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("http://spdx.org/licenses/%s", + token); + return gs_details_page_label_widget (self, token, uri); + } + + /* nothing to show */ + return NULL; +} + +static void +gs_details_page_license_free_cb (GtkWidget *widget, GsDetailsPage *self) +{ + guint cnt = 0; + guint i; + g_auto(GStrv) tokens = NULL; + + /* URLify any SPDX IDs */ + gs_container_remove_all (GTK_CONTAINER (self->box_details_license_list)); + tokens = as_utils_spdx_license_tokenize (gs_app_get_license (self->app)); + for (i = 0; tokens[i] != NULL; i++) { + GtkWidget *w = NULL; + + /* translated join */ + if (g_strcmp0 (tokens[i], "&") == 0) + continue; + if (g_strcmp0 (tokens[i], "|") == 0) + continue; + if (g_strcmp0 (tokens[i], "+") == 0) + continue; + + /* add widget */ + w = gs_details_page_license_widget_for_token (self, tokens[i]); + if (w == NULL) + continue; + gtk_container_add (GTK_CONTAINER (self->box_details_license_list), w); + + /* one more license */ + cnt++; + } + + /* use the correct plural */ + gtk_label_set_label (GTK_LABEL (self->label_licenses_intro), + /* TRANSLATORS: for the free software popover */ + ngettext ("Users are bound by the following license:", + "Users are bound by the following licenses:", + cnt)); + gtk_widget_set_visible (self->label_licenses_intro, cnt > 0); + + gtk_widget_show (self->popover_license_free); +} + +static void +gs_details_page_license_nonfree_cb (GtkWidget *widget, GsDetailsPage *self) +{ + g_autofree gchar *str = NULL; + g_autofree gchar *uri = NULL; + g_auto(GStrv) tokens = NULL; + + /* license specified as a link */ + tokens = as_utils_spdx_license_tokenize (gs_app_get_license (self->app)); + for (guint i = 0; tokens[i] != NULL; i++) { + if (g_str_has_prefix (tokens[i], "@LicenseRef-proprietary=")) { + uri = g_strdup (tokens[i] + 24); + break; + } + } + if (uri == NULL) + uri = g_settings_get_string (self->settings, "nonfree-software-uri"); + str = g_strdup_printf ("<a href=\"%s\">%s</a>", + uri, + _("More information")); + gtk_label_set_label (GTK_LABEL (self->label_license_nonfree_details), str); + gtk_widget_show (self->popover_license_nonfree); +} + +static void +gs_details_page_license_unknown_cb (GtkWidget *widget, GsDetailsPage *self) +{ + gtk_widget_show (self->popover_license_unknown); +} + +static void +gs_details_page_network_available_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsDetailsPage *self) +{ + gs_details_page_refresh_reviews (self); +} + +static void +gs_details_page_star_pressed_cb(GtkWidget *widget, GdkEventButton *event, GsDetailsPage *self) +{ + gs_details_page_write_review_cb(GTK_BUTTON (self->button_review), self); +} + +static gboolean +gs_details_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (page); + GtkAdjustment *adj; + GtkWidget *origin_popover_list_box; + + g_return_val_if_fail (GS_IS_DETAILS_PAGE (self), TRUE); + + self->shell = shell; + + self->plugin_loader = g_object_ref (plugin_loader); + self->builder = g_object_ref (builder); + self->cancellable = g_object_ref (cancellable); + + /* show review widgets if we have plugins that provide them */ + self->enable_reviews = + gs_plugin_loader_get_plugin_supported (plugin_loader, + "gs_plugin_review_submit"); + g_signal_connect (self->button_review, "clicked", + G_CALLBACK (gs_details_page_write_review_cb), + self); + g_signal_connect (self->star_eventbox, "button-press-event", + G_CALLBACK (gs_details_page_star_pressed_cb), + self); + + /* hide some UI when offline */ + g_signal_connect_object (self->plugin_loader, "notify::network-available", + G_CALLBACK (gs_details_page_network_available_notify_cb), + self, 0); + + /* setup details */ + g_signal_connect (self->button_install, "clicked", + G_CALLBACK (gs_details_page_app_install_button_cb), + self); + g_signal_connect (self->button_update, "clicked", + G_CALLBACK (gs_details_page_app_update_button_cb), + self); + g_signal_connect (self->button_remove, "clicked", + G_CALLBACK (gs_details_page_app_remove_button_cb), + self); + g_signal_connect (self->button_cancel, "clicked", + G_CALLBACK (gs_details_page_app_cancel_button_cb), + self); + g_signal_connect (self->button_more_reviews, "clicked", + G_CALLBACK (gs_details_page_more_reviews_button_cb), + self); + g_signal_connect (self->button_details_rating_value, "clicked", + G_CALLBACK (gs_details_page_content_rating_button_cb), + self); + g_signal_connect (self->button_details_permissions_value, "clicked", + G_CALLBACK (gs_details_page_permissions_button_cb), + self); + g_signal_connect (self->label_details_updated_value, "activate-link", + G_CALLBACK (gs_details_page_history_cb), + self); + g_signal_connect (self->button_details_launch, "clicked", + G_CALLBACK (gs_details_page_app_launch_button_cb), + self); + g_signal_connect (self->button_details_add_shortcut, "clicked", + G_CALLBACK (gs_details_page_app_add_shortcut_button_cb), + self); + g_signal_connect (self->button_details_remove_shortcut, "clicked", + G_CALLBACK (gs_details_page_app_remove_shortcut_button_cb), + self); + g_signal_connect (self->button_details_website, "clicked", + G_CALLBACK (gs_details_page_website_cb), + self); + g_signal_connect (self->button_donate, "clicked", + G_CALLBACK (gs_details_page_donate_cb), + self); + g_signal_connect (self->button_details_license_free, "clicked", + G_CALLBACK (gs_details_page_license_free_cb), + self); + g_signal_connect (self->button_details_license_nonfree, "clicked", + G_CALLBACK (gs_details_page_license_nonfree_cb), + self); + g_signal_connect (self->button_details_license_unknown, "clicked", + G_CALLBACK (gs_details_page_license_unknown_cb), + self); + g_signal_connect (self->label_license_nonfree_details, "activate-link", + G_CALLBACK (gs_details_page_activate_link_cb), + self); + origin_popover_list_box = GTK_WIDGET (gtk_builder_get_object (self->builder, "origin_popover_list_box")); + gtk_list_box_set_sort_func (GTK_LIST_BOX (origin_popover_list_box), + origin_popover_list_sort_func, + NULL, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (origin_popover_list_box), + list_header_func, + NULL, NULL); + g_signal_connect (origin_popover_list_box, "row-activated", + G_CALLBACK (origin_popover_row_activated_cb), + self); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details)); + gtk_container_set_focus_vadjustment (GTK_CONTAINER (self->box_details), adj); + return TRUE; +} + +static void +gs_details_page_dispose (GObject *object) +{ + GsDetailsPage *self = GS_DETAILS_PAGE (object); + + stop_progress_pulsing (self); + + if (self->app != NULL) { + g_signal_handlers_disconnect_by_func (self->app, gs_details_page_notify_state_changed_cb, self); + g_signal_handlers_disconnect_by_func (self->app, gs_details_page_progress_changed_cb, self); + g_clear_object (&self->app); + } + g_clear_object (&self->app_local_file); + g_clear_object (&self->builder); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + g_clear_object (&self->app_cancellable); + g_clear_object (&self->session); + g_clear_object (&self->size_group_origin_popover); + g_clear_object (&self->button_details_rating_style_provider); + + G_OBJECT_CLASS (gs_details_page_parent_class)->dispose (object); +} + +static void +gs_details_page_class_init (GsDetailsPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_details_page_dispose; + page_class->app_installed = gs_details_page_app_installed; + page_class->app_removed = gs_details_page_app_removed; + page_class->switch_to = gs_details_page_switch_to; + page_class->reload = gs_details_page_reload; + page_class->setup = gs_details_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-details-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_icon); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_summary); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, application_details_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_addons); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_description); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_support); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress2); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, star); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_review_count); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_main); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_scrolledwindow); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_thumbnails); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_license_list); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_launch); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_add_shortcut); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_remove_shortcut); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_website); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_donate); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_install); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_update); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_remove); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_cancel); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_more_reviews); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_norepo); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_repo); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_package_baseos); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_repo); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_addons_uninstalled_app); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_progress_percentage); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_progress_status); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_category_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_category_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_developer_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_developer_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_developer); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_developer_verified); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_free); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_nonfree); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_license_unknown); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_license_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_license_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_origin_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_origin_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_download_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_download_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_installed_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_installed_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_permissions_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_permissions_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_failed); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_addons); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_reviews); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_fallback); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, histogram); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_review); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_reviews); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, scrolledwindow_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, spinner_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, spinner_remove); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, stack_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, grid_details_kudo); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_docs); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_sandboxed); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_integration); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_translated); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, image_details_kudo_updated); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_docs); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_sandboxed); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_integration); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_translated); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_kudo_updated); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, progressbar_top); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_free); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_nonfree); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_license_unknown); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_license_nonfree_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_licenses_intro); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_content_rating); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_message); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_none); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_rating_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_rating_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_permissions); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_permissions_details); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, star_eventbox); +} + +static void +gs_details_page_init (GsDetailsPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + /* setup networking */ + self->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (), + NULL); + self->settings = g_settings_new ("org.gnome.software"); + g_signal_connect_swapped (self->settings, "changed", + G_CALLBACK (settings_changed_cb), + self); + self->size_group_origin_popover = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_addons), + list_header_func, + self, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_addons), + list_sort_func, + self, NULL); +} + +GsDetailsPage * +gs_details_page_new (void) +{ + GsDetailsPage *self; + self = g_object_new (GS_TYPE_DETAILS_PAGE, NULL); + return GS_DETAILS_PAGE (self); +} diff --git a/src/gs-details-page.h b/src/gs-details-page.h new file mode 100644 index 0000000..0c5eda1 --- /dev/null +++ b/src/gs-details-page.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_DETAILS_PAGE (gs_details_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsDetailsPage, gs_details_page, GS, DETAILS_PAGE, GsPage) + +GsDetailsPage *gs_details_page_new (void); +void gs_details_page_set_app (GsDetailsPage *self, + GsApp *app); +void gs_details_page_set_local_file(GsDetailsPage *self, + GFile *file); +void gs_details_page_set_url (GsDetailsPage *self, + const gchar *url); +GsApp *gs_details_page_get_app (GsDetailsPage *self); + +G_END_DECLS diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui new file mode 100644 index 0000000..f86674f --- /dev/null +++ b/src/gs-details-page.ui @@ -0,0 +1,1424 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsDetailsPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="details-accessible"> + <property name="accessible-name" translatable="yes">Details page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack_details"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="details_spinner_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkSpinner" id="spinner_details"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + </child> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_details"> + <property name="visible">True</property> + <property name="shadow_type">none</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <child> + <object class="GtkBox" id="box_details"> + <property name="width_request">752</property> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="margin_top">4</property> + <property name="margin_bottom">4</property> + <property name="border_width">24</property> + <property name="spacing">18</property> + <property name="hexpand">False</property> + <child> + <object class="GtkBox" id="box_details_header"> + <property name="orientation">horizontal</property> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="application_details_icon"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="pixel_size">96</property> + <style> + <class name="icon-dropshadow"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_header2"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="margin_start">24</property> + <property name="margin_end">24</property> + <child> + <object class="GtkLabel" id="application_details_title"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="margin_bottom">4</property> + <property name="xalign">0</property> + <property name="selectable">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">20</property> + <style> + <class name="application-details-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="application_details_summary"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="margin_bottom">16</property> + <property name="xalign">0</property> + <property name="selectable">True</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <style> + <class name="application-details-summary"/> + </style> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox" id="star_box"> + <property name="visible">True</property> + <property name="valign">start</property> + <child> + <object class="GtkEventBox" id="star_eventbox"> + <property name="visible">True</property> + <child> + <object class="GsStarWidget" id="star"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label_review_count"> + <property name="visible">True</property> + <property name="margin_start">5</property> + <property name="halign">start</property> + <property name="valign">center</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + + </object> + </child> + + <child> + <object class="GtkBox" id="box_install_remove"> + <property name="visible">True</property> + <property name="margin_bottom">14</property> + <property name="spacing">9</property> + <child> + <object class="GtkButton" id="button_install"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes">_Install</property> + <property name="width_request">105</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_details_launch"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes">_Remove</property> + <property name="width_request">105</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + </object> + </child> + <child> + <object class="GtkSpinner" id="spinner_remove"> + <property name="visible">False</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkBox" id="box_progress"> + <property name="visible">True</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <property name="hexpand">True</property> + <property name="valign">center</property> + <child> + <object class="GtkBox" id="box_progress2"> + <property name="visible">True</property> + <property name="spacing">3</property> + <property name="halign">center</property> + <child> + <object class="GtkLabel" id="label_progress_status"> + <property name="visible">True</property> + <property name="valign">start</property> + <property name="label" translatable="yes">Downloading</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_progress_percentage"> + <property name="visible">True</property> + <property name="valign">start</property> + <property name="label">50%</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkProgressBar" id="progressbar_top"> + <property name="visible">True</property> + <property name="fraction">0.5</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="width_request">624</property> + <style> + <class name="upgrade-progressbar"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes">_Cancel</property> + <property name="width_request">116</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_update"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes">_Update</property> + <property name="width_request">105</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_details_add_shortcut"> + <property name="visible">False</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes" comments="Translators: A label for a button to add a shortcut to the selected application.">_Add shortcut</property> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_details_remove_shortcut"> + <property name="visible">False</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes" comments="Translators: A label for a button to remove a shortcut to the selected application.">Re_move shortcut</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + + <child> + <object class="GtkBox" id="box_details_screenshot"> + <property name="visible">True</property> + <property name="margin_bottom">14</property> + <property name="spacing">9</property> + <child> + <object class="GtkBox" id="box_details_screenshot_main"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="hexpand">True</property> + <child> + <placeholder/> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow" id="box_details_screenshot_scrolledwindow"> + <property name="visible">True</property> + <property name="shadow_type">none</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkBox" id="box_details_screenshot_thumbnails"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">7</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_screenshot_fallback"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="width_request">752</property> + <property name="height_request">423</property> + <property name="hexpand">True</property> + <property name="halign">fill</property> + <style> + <class name="screenshot-image"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="pixel_size">64</property> + <property name="icon_name">camera-photo-symbolic</property> + <property name="icon_size">6</property> + <property name="valign">end</property> + <property name="vexpand">True</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">start</property> + <property name="vexpand">True</property> + <property name="label" translatable="yes">No screenshot provided</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_description"> + <property name="visible">True</property> + <property name="margin_bottom">14</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <placeholder/> + </child> + </object> + </child> + <child> + <object class="GsInfoBar" id="infobar_details_app_repo"> + <property name="visible">True</property> + <property name="margin_top">20</property> + <property name="title" translatable="yes">Software Repository Included</property> + <property name="body" translatable="yes">This application includes a software repository which provides updates, as well as access to other software.</property> + </object> + </child> + <child> + <object class="GsInfoBar" id="infobar_details_app_norepo"> + <property name="visible">True</property> + <property name="margin_top">20</property> + <property name="title" translatable="yes">No Software Repository Included</property> + <property name="body" translatable="yes">This application does not include a software repository. It will not be updated with new versions.</property> + </object> + </child> + <child> + <object class="GsInfoBar" id="infobar_details_package_baseos"> + <property name="visible">True</property> + <property name="message_type">warning</property> + <property name="margin_top">20</property> + <property name="title" translatable="yes">This software is already provided by your distribution and should not be replaced.</property> + </object> + </child> + <child> + <object class="GsInfoBar" id="infobar_details_repo"> + <property name="visible">True</property> + <property name="margin_top">20</property> + <property name="title" translatable="yes" comments="Translators: a repository file used for installing software has been discovered.">Software Repository Identified</property> + <property name="body" translatable="yes">Adding this software repository will give you access to additional software and upgrades.</property> + <property name="warning" translatable="yes">Only use software repositories that you trust.</property> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_support"> + <property name="visible">True</property> + <property name="margin_bottom">26</property> + <property name="spacing">12</property> + <child> + <object class="GtkButton" id="button_details_website"> + <property name="label" translatable="yes">_Website</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_donate"> + <property name="label" translatable="yes">_Donate</property> + <property name="width_request">150</property> + <property name="visible">False</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="application_details_details_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Details</property> + <style> + <class name="application-details-title"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_details"> + <property name="visible">True</property> + <property name="margin_bottom">26</property> + <property name="spacing">30</property> + <child> + <object class="GtkGrid" id="grid_details_kudo"> + <property name="visible">True</property> + <property name="row_spacing">9</property> + <property name="column_spacing">12</property> + <property name="hexpand">True</property> + <child> + <object class="GtkImage" id="image_details_kudo_translated"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">preferences-desktop-locale-symbolic</property> + <property name="icon_size">6</property> + <style> + <class name="kudo-pill"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image_details_kudo_docs"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">system-help-symbolic</property> + <style> + <class name="kudo-pill"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image_details_kudo_updated"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">software-update-available-symbolic</property> + <property name="icon_size">6</property> + <style> + <class name="kudo-pill"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image_details_kudo_integration"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">emblem-system-symbolic</property> + <property name="icon_size">0</property> + <style> + <class name="kudo-pill"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image_details_kudo_sandboxed"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">security-medium-symbolic</property> + <property name="icon_size">0</property> + <style> + <class name="kudo-pill"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_kudo_translated"> + <property name="visible">True</property> + <property name="label" translatable="yes">Localized in your Language</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_kudo_docs"> + <property name="visible">True</property> + <property name="label" translatable="yes">Documentation</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_kudo_updated"> + <property name="visible">True</property> + <property name="label" translatable="yes">Release Activity</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_kudo_integration"> + <property name="visible">True</property> + <property name="label" translatable="yes">System Integration</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_kudo_sandboxed"> + <property name="visible">True</property> + <property name="label" translatable="yes">Sandboxed</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkGrid" id="grid_details_details"> + <property name="visible">True</property> + <property name="row_spacing">9</property> + <property name="column_spacing">24</property> + <property name="hexpand">True</property> + <property name="valign">start</property> + <child> + <object class="GtkLabel" id="label_details_channel_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Channel</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_channel_value"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="label">stable</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <accessibility> + <relation type="labelled-by" target="label_details_channel_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_version_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Version</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_version_value"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="label">0.12.3</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <accessibility> + <relation type="labelled-by" target="label_details_version_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_rating_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Age Rating</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">9</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_details_rating_value"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label">18</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="vexpand">False</property> + <property name="halign">start</property> + <style> + <class name="content-rating"/> + </style> + <accessibility> + <relation type="labelled-by" target="label_details_version_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">9</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_permissions_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Permissions</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">10</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_details_permissions_value"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="label">Details</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="vexpand">False</property> + <property name="halign">start</property> + <style> + <class name="content-rating"/> + </style> + <accessibility> + <relation type="labelled-by" target="label_details_permissions_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">10</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_updated_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Updated</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_updated_value"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="label">May 12, 2012</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <accessibility> + <relation type="labelled-by" target="label_details_updated_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_category_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Category</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_category_value"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="label">Photos & Video</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + <property name="max_width_chars">10</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <accessibility> + <relation type="labelled-by" target="label_details_category_title"/> + </accessibility> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_size_installed_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Installed Size</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">7</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_size_installed_value"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="label">30 MB</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">7</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_size_download_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Download Size</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">8</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_size_download_value"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="label">30 MB</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">8</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="label_details_origin_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Source</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_origin_value"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="label">Upstream</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_developer_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">Developer</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_details_developer"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="spacing">3</property> + <child> + <object class="GtkLabel" id="label_details_developer_value"> + <property name="visible">True</property> + <property name="label">Yorba</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + <property name="max_width_chars">100</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="hexpand">False</property> + </object> + </child> + <child> + <object class="GtkImage" id="image_details_developer_verified"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">emblem-ok-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_details_license_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">License</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_details_license_value"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButton" id="button_details_license_free"> + <property name="label" translatable="yes" comments="This refers to the license of the application">Free</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <style> + <class name="details-license-free"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_details_license_nonfree"> + <property name="label" translatable="yes" comments="This refers to the license of the application">Proprietary</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <style> + <class name="details-license-nonfree"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_details_license_unknown"> + <property name="label" translatable="yes" comments="This refers to the license of the application">Unknown</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <style> + <class name="details-license-unknown"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box_addons"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin_bottom">26</property> + + <child> + <object class="GtkBox" id="box_addons_title"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin_bottom">18</property> + <child> + <object class="GtkLabel" id="label_addons_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Add-ons</property> + <style> + <class name="application-details-title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label_addons_uninstalled_app"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + <property name="max-width-chars">40</property> + <property name="label" translatable="yes">Selected add-ons will be installed with the application.</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkFrame" id="box_addons_frame"> + <property name="visible">True</property> + <property name="shadow_type">in</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <style> + <class name="view"/> + </style> + <child> + <object class="GtkListBox" id="list_box_addons"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box_reviews"> + <property name="visible">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="application_details_reviews_title"> + <property name="visible">True</property> + <property name="margin_bottom">18</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: Header of the section with other users' opinions about the app.">Reviews</property> + <style> + <class name="application-details-title"/> + </style> + </object> + </child> + <child> + <object class="GsReviewHistogram" id="histogram"> + <property name="visible">False</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="margin_bottom">32</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_review"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes" comments="Translators: Button opening a dialog where the users can write and publish their opinions about the apps.">_Write a Review</property> + <property name="can_focus">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="margin_bottom">18</property> + </object> + </child> + <child> + <object class="GtkListBox" id="list_box_reviews"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + <style> + <class name="review-listbox"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_more_reviews"> + <property name="visible">False</property> + <property name="use_underline">True</property> + <property name="label" translatable="yes" comments="Translators: Button to return more application-submitted reviews.">_Show More</property> + <property name="can_focus">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="margin_top">12</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">ready</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_failed"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">24</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_failed"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_failed"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <property name="justify">center</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="name">failed</property> + </packing> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup" id="sizegroup_install_remove"> + <property name="ignore_hidden">True</property> + <widgets> + <widget name="button_install"/> + <widget name="button_update"/> + <widget name="button_remove"/> + <widget name="button_details_launch"/> + </widgets> + </object> + <object class="GtkSizeGroup" id="sizegroup_details_buttons"> + <widgets> + <widget name="button_details_website"/> + </widgets> + </object> + <object class="GtkSizeGroup" id="sizegroup_details_title"> + <property name="ignore_hidden">True</property> + <widgets> + <widget name="label_details_version_title"/> + <widget name="label_details_updated_title"/> + <widget name="label_details_category_title"/> + <widget name="label_details_origin_title"/> + <widget name="label_details_license_title"/> + <widget name="label_details_size_installed_title"/> + <widget name="label_details_size_download_title"/> + <widget name="label_details_developer_title"/> + </widgets> + </object> + <object class="GtkSizeGroup" id="sizegroup_details_value"> + <property name="ignore_hidden">True</property> + <widgets> + <widget name="label_details_version_value"/> + <widget name="label_details_updated_value"/> + <widget name="label_details_category_value"/> + <widget name="label_details_size_installed_value"/> + <widget name="label_details_size_download_value"/> + <widget name="box_details_developer"/> + <widget name="button_details_license_free"/> + </widgets> + </object> + <object class="GtkSizeGroup" id="sizegroup_button_details_license"> + <widgets> + <widget name="button_details_license_free"/> + <widget name="button_details_license_nonfree"/> + <widget name="button_details_license_unknown"/> + </widgets> + </object> + <object class="GtkPopover" id="popover_license_free"> + <property name="visible">False</property> + <property name="border_width">21</property> + <property name="relative_to">button_details_license_free</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Free Software</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="max_width_chars">34</property> + <property name="label" translatable="yes">This means that the software can be freely run, copied, distributed, studied and modified.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_licenses_intro"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label">Users are bound by the following licenses:</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkBox" id="box_details_license_list"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="popover_license_nonfree"> + <property name="visible">False</property> + <property name="border_width">21</property> + <property name="relative_to">button_details_license_nonfree</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Proprietary Software</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="max_width_chars">34</property> + <property name="label" translatable="yes">This means that the software is owned by an individual or a company. There are often restrictions on its use and its source code cannot usually be accessed.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_license_nonfree_details"> + <property name="visible">True</property> + <property name="use-markup">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">More information</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="popover_license_unknown"> + <property name="visible">False</property> + <property name="name">popover_license_unknown</property> + <property name="border_width">21</property> + <property name="relative_to">button_details_license_unknown</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Unknown Software License</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="max_width_chars">34</property> + <property name="label" translatable="yes">The license terms of this software are unknown.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + </object> + + <object class="GtkPopover" id="popover_content_rating"> + <property name="visible">False</property> + <child> + <object class="GtkBox" id="box_content_rating"> + <property name="visible">True</property> + <property name="border_width">24</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="label_content_rating_title"> + <property name="visible">True</property> + <property name="label" translatable="yes">The application was rated this way because it features:</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_content_rating_message"> + <property name="visible">True</property> + <property name="label">• Use of alcoholic beverages</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_content_rating_none"> + <property name="visible">True</property> + <property name="label" translatable="yes">No details were available for this rating.</property> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="popover_permissions"> + <property name="visible">False</property> + <property name="border_width">21</property> + <property name="relative_to">button_details_permissions_value</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="label" translatable="yes">Permissions</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkBox" id="box_permissions_details"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/gs-extras-page.c b/src/gs-extras-page.c new file mode 100644 index 0000000..9b9aeec --- /dev/null +++ b/src/gs-extras-page.c @@ -0,0 +1,1220 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-extras-page.h" + +#include "gs-app-row.h" +#include "gs-language.h" +#include "gs-shell.h" +#include "gs-common.h" +#include "gs-utils.h" +#include "gs-vendor.h" + +#include <glib/gi18n.h> + +typedef enum { + GS_EXTRAS_PAGE_STATE_LOADING, + GS_EXTRAS_PAGE_STATE_READY, + GS_EXTRAS_PAGE_STATE_NO_RESULTS, + GS_EXTRAS_PAGE_STATE_FAILED +} GsExtrasPageState; + +typedef struct { + gchar *title; + gchar *search; + gchar *search_filename; + gchar *package_filename; + gchar *url_not_found; + GsExtrasPage *self; +} SearchData; + +struct _GsExtrasPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *search_cancellable; + GsShell *shell; + GsExtrasPageState state; + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + GPtrArray *array_search_data; + GsExtrasPageMode mode; + GsLanguage *language; + GsVendor *vendor; + guint pending_search_cnt; + + GtkWidget *label_failed; + GtkWidget *label_no_results; + GtkWidget *list_box_results; + GtkWidget *scrolledwindow; + GtkWidget *spinner; + GtkWidget *stack; +}; + +G_DEFINE_TYPE (GsExtrasPage, gs_extras_page, GS_TYPE_PAGE) + +static void +search_data_free (SearchData *search_data) +{ + if (search_data->self != NULL) + g_object_unref (search_data->self); + g_free (search_data->title); + g_free (search_data->search); + g_free (search_data->search_filename); + g_free (search_data->package_filename); + g_free (search_data->url_not_found); + g_slice_free (SearchData, search_data); +} + +static GsExtrasPageMode +gs_extras_page_mode_from_string (const gchar *str) +{ + if (g_strcmp0 (str, "install-package-files") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES; + if (g_strcmp0 (str, "install-provide-files") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES; + if (g_strcmp0 (str, "install-package-names") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES; + if (g_strcmp0 (str, "install-mime-types") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES; + if (g_strcmp0 (str, "install-fontconfig-resources") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES; + if (g_strcmp0 (str, "install-gstreamer-resources") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES; + if (g_strcmp0 (str, "install-plasma-resources") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES; + if (g_strcmp0 (str, "install-printer-drivers") == 0) + return GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS; + + g_assert_not_reached (); +} + +const gchar * +gs_extras_page_mode_to_string (GsExtrasPageMode mode) +{ + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES) + return "install-package-files"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES) + return "install-provide-files"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES) + return "install-package-names"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES) + return "install-mime-types"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES) + return "install-fontconfig-resources"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES) + return "install-gstreamer-resources"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES) + return "install-plasma-resources"; + if (mode == GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS) + return "install-printer-drivers"; + + g_assert_not_reached (); +} + +static gchar * +build_comma_separated_list (gchar **items) +{ + guint len; + + len = g_strv_length (items); + if (len == 2) { + /* TRANSLATORS: separator for a list of items */ + return g_strjoinv (_(" and "), items); + } else { + /* TRANSLATORS: separator for a list of items */ + return g_strjoinv (_(", "), items); + } +} + +static gchar * +build_title (GsExtrasPage *self) +{ + guint i; + g_autofree gchar *titles = NULL; + g_autoptr(GPtrArray) title_array = NULL; + + title_array = g_ptr_array_new (); + for (i = 0; i < self->array_search_data->len; i++) { + SearchData *search_data; + + search_data = g_ptr_array_index (self->array_search_data, i); + g_ptr_array_add (title_array, search_data->title); + } + g_ptr_array_add (title_array, NULL); + + titles = build_comma_separated_list ((gchar **) title_array->pdata); + + switch (self->mode) { + case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES: + /* TRANSLATORS: Application window title for fonts installation. + %s will be replaced by name of the script we're searching for. */ + return g_strdup_printf (ngettext ("Available fonts for the %s script", + "Available fonts for the %s scripts", + self->array_search_data->len), + titles); + break; + default: + /* TRANSLATORS: Application window title for codec installation. + %s will be replaced by actual codec name(s) */ + return g_strdup_printf (ngettext ("Available software for %s", + "Available software for %s", + self->array_search_data->len), + titles); + break; + } +} + +static void +gs_extras_page_update_ui_state (GsExtrasPage *self) +{ + GtkWidget *widget; + g_autofree gchar *title = NULL; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS) + return; + + /* main spinner */ + switch (self->state) { + case GS_EXTRAS_PAGE_STATE_LOADING: + gs_start_spinner (GTK_SPINNER (self->spinner)); + break; + case GS_EXTRAS_PAGE_STATE_READY: + case GS_EXTRAS_PAGE_STATE_NO_RESULTS: + case GS_EXTRAS_PAGE_STATE_FAILED: + gs_stop_spinner (GTK_SPINNER (self->spinner)); + break; + default: + g_assert_not_reached (); + break; + } + + /* headerbar title */ + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header")); + switch (self->state) { + case GS_EXTRAS_PAGE_STATE_LOADING: + case GS_EXTRAS_PAGE_STATE_READY: + title = build_title (self); + gtk_label_set_label (GTK_LABEL (widget), title); + break; + case GS_EXTRAS_PAGE_STATE_NO_RESULTS: + case GS_EXTRAS_PAGE_STATE_FAILED: + gtk_label_set_label (GTK_LABEL (widget), _("Unable to Find Requested Software")); + break; + default: + g_assert_not_reached (); + break; + } + + /* stack */ + switch (self->state) { + case GS_EXTRAS_PAGE_STATE_LOADING: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "spinner"); + break; + case GS_EXTRAS_PAGE_STATE_READY: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "results"); + break; + case GS_EXTRAS_PAGE_STATE_NO_RESULTS: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "no-results"); + break; + case GS_EXTRAS_PAGE_STATE_FAILED: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack), "failed"); + break; + default: + g_assert_not_reached (); + break; + } +} + +static void +gs_extras_page_set_state (GsExtrasPage *self, + GsExtrasPageState state) +{ + self->state = state; + gs_extras_page_update_ui_state (self); +} + +static void +app_row_button_clicked_cb (GsAppRow *app_row, + GsExtrasPage *self) +{ + GsApp *app = gs_app_row_get_app (app_row); + + if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE && + gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) { + gs_shell_show_uri (self->shell, + gs_app_get_url (app, AS_URL_KIND_MISSING)); + } else if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE || + gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL || + gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) { + gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL, + self->search_cancellable); + } else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + gs_page_remove_app (GS_PAGE (self), app, self->search_cancellable); + } else { + g_critical ("extras: app in unexpected state %u", gs_app_get_state (app)); + } +} + +static void +gs_extras_page_add_app (GsExtrasPage *self, GsApp *app, GsAppList *list, SearchData *search_data) +{ + GtkWidget *app_row; + g_autoptr(GList) existing_apps = NULL; + + /* Don't add same app twice */ + existing_apps = gtk_container_get_children (GTK_CONTAINER (self->list_box_results)); + for (GList *l = existing_apps; l != NULL; l = l->next) { + GsApp *existing_app; + + existing_app = gs_app_row_get_app (GS_APP_ROW (l->data)); + if (app == existing_app) + gtk_container_remove (GTK_CONTAINER (self->list_box_results), + GTK_WIDGET (l->data)); + } + + app_row = gs_app_row_new (app); + gs_app_row_set_colorful (GS_APP_ROW (app_row), TRUE); + gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE); + + g_object_set_data_full (G_OBJECT (app_row), "missing-title", g_strdup (search_data->title), g_free); + + g_signal_connect (app_row, "button-clicked", + G_CALLBACK (app_row_button_clicked_cb), + self); + + gtk_container_add (GTK_CONTAINER (self->list_box_results), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button); + gtk_widget_show (app_row); +} + +static GsApp * +create_missing_app (SearchData *search_data) +{ + GsExtrasPage *self = search_data->self; + GsApp *app; + GString *summary_missing; + g_autofree gchar *name = NULL; + g_autofree gchar *url = NULL; + + app = gs_app_new ("missing-codec"); + + /* TRANSLATORS: This string is used for codecs that weren't found */ + name = g_strdup_printf (_("%s not found"), search_data->title); + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, name); + + /* TRANSLATORS: hyperlink title */ + url = g_strdup_printf ("<a href=\"%s\">%s</a>", search_data->url_not_found, _("on the website")); + + summary_missing = g_string_new (""); + switch (self->mode) { + case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No applications are available that provide the file %s."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get missing applications " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get missing applications " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("%s is not available."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get missing applications " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No applications are available for %s support."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get an application that can support this format " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No fonts are available for the %s script support."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get additional fonts " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No addon codecs are available for the %s format."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get a codec that can play this format " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No Plasma resources are available for %s support."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get additional Plasma resources " + "might be found %s."), search_data->title, url); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS: + /* TRANSLATORS: this is when we know about an application or + * addon, but it can't be listed for some reason */ + g_string_append_printf (summary_missing, _("No printer drivers are available for %s."), search_data->title); + g_string_append (summary_missing, "\n"); + /* TRANSLATORS: first %s is the codec name, and second %s is a + * hyperlink with the "on the website" text */ + g_string_append_printf (summary_missing, _("Information about %s, as well as options " + "for how to get a driver that supports this printer " + "might be found %s."), search_data->title, url); + + break; + default: + g_assert_not_reached (); + break; + } + gs_app_set_summary_missing (app, g_string_free (summary_missing, FALSE)); + + gs_app_set_kind (app, AS_APP_KIND_GENERIC); + gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE); + gs_app_set_url (app, AS_URL_KIND_MISSING, search_data->url_not_found); + + return app; +} + +static gchar * +build_no_results_label (GsExtrasPage *self) +{ + GsApp *app = NULL; + guint num; + g_autofree gchar *codec_titles = NULL; + g_autofree gchar *url = NULL; + g_autoptr(GList) list = NULL; + g_autoptr(GPtrArray) array = NULL; + + list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results)); + num = g_list_length (list); + + g_assert (num > 0); + + array = g_ptr_array_new (); + for (GList *l = list; l != NULL; l = l->next) { + app = gs_app_row_get_app (GS_APP_ROW (l->data)); + g_ptr_array_add (array, + g_object_get_data (G_OBJECT (l->data), "missing-title")); + } + g_ptr_array_add (array, NULL); + + url = g_strdup_printf ("<a href=\"%s\">%s</a>", + gs_app_get_url (app, AS_URL_KIND_MISSING), + /* TRANSLATORS: hyperlink title */ + _("this website")); + + codec_titles = build_comma_separated_list ((gchar **) array->pdata); + /* TRANSLATORS: no codecs were found. First %s will be replaced by actual codec name(s), second %s is a link titled "this website" */ + return g_strdup_printf (ngettext ("Unfortunately, the %s you were searching for could not be found. Please see %s for more information.", + "Unfortunately, the %s you were searching for could not be found. Please see %s for more information.", + num), + codec_titles, + url); +} + +static void +show_search_results (GsExtrasPage *self) +{ + GsApp *app; + guint n_children; + guint n_missing; + g_autoptr(GList) list = NULL; + + list = gtk_container_get_children (GTK_CONTAINER (self->list_box_results)); + n_children = g_list_length (list); + + /* count the number of rows with missing codecs */ + n_missing = 0; + for (GList *l = list; l != NULL; l = l->next) { + app = gs_app_row_get_app (GS_APP_ROW (l->data)); + if (g_strcmp0 (gs_app_get_id (app), "missing-codec") == 0) { + n_missing++; + } + } + + if (n_children == 0 || n_children == n_missing) { + g_autofree gchar *str = NULL; + + /* no results */ + g_debug ("extras: failed to find any results, %u", n_missing); + str = build_no_results_label (self); + gtk_label_set_label (GTK_LABEL (self->label_no_results), str); + gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_NO_RESULTS); + } else if (n_children == 1) { + /* switch directly to details view */ + g_debug ("extras: found one result, showing in details view"); + g_assert (list != NULL); + app = gs_app_row_get_app (GS_APP_ROW (list->data)); + gs_shell_change_mode (self->shell, GS_SHELL_MODE_DETAILS, app, TRUE); + } else { + /* show what we got */ + g_debug ("extras: got %u search results, showing", n_children); + gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_READY); + } +} + +static void +search_files_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SearchData *search_data = (SearchData *) user_data; + GsExtrasPage *self = search_data->self; + g_autoptr(GsAppList) list = NULL; + guint i; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + g_autofree gchar *str = NULL; + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("extras: search files cancelled"); + return; + } + g_warning ("failed to find any search results: %s", error->message); + str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message); + gtk_label_set_label (GTK_LABEL (self->label_failed), str); + gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED); + return; + } + + /* add missing item */ + if (gs_app_list_length (list) == 0) { + g_autoptr(GsApp) app = NULL; + g_debug ("extras: no search result for %s, showing as missing", + search_data->title); + app = create_missing_app (search_data); + gs_app_list_add (list, app); + } + + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + g_debug ("%s\n\n", gs_app_to_string (app)); + gs_extras_page_add_app (self, app, list, search_data); + } + + self->pending_search_cnt--; + + /* have all searches finished? */ + if (self->pending_search_cnt == 0) + show_search_results (self); +} + +static void +file_to_app_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SearchData *search_data = (SearchData *) user_data; + GsExtrasPage *self = search_data->self; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("extras: search what provides cancelled"); + return; + } + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED)) { + g_debug ("extras: no search result for %s, showing as missing", search_data->title); + app = create_missing_app (search_data); + } else { + g_autofree gchar *str = NULL; + + g_warning ("failed to find any search results: %s", error->message); + str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message); + gtk_label_set_label (GTK_LABEL (self->label_failed), str); + gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED); + return; + } + } else { + app = g_object_ref (gs_app_list_index (list, 0)); + } + + g_debug ("%s\n\n", gs_app_to_string (app)); + gs_extras_page_add_app (self, app, list, search_data); + + self->pending_search_cnt--; + + /* have all searches finished? */ + if (self->pending_search_cnt == 0) + show_search_results (self); +} + +static void +get_search_what_provides_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SearchData *search_data = (SearchData *) user_data; + GsExtrasPage *self = search_data->self; + g_autoptr(GsAppList) list = NULL; + guint i; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + g_autofree gchar *str = NULL; + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("extras: search what provides cancelled"); + return; + } + g_warning ("failed to find any search results: %s", error->message); + str = g_strdup_printf ("%s: %s", _("Failed to find any search results"), error->message); + gtk_label_set_label (GTK_LABEL (self->label_failed), str); + gs_extras_page_set_state (self, GS_EXTRAS_PAGE_STATE_FAILED); + return; + } + + /* add missing item */ + if (gs_app_list_length (list) == 0) { + g_autoptr(GsApp) app = NULL; + g_debug ("extras: no search result for %s, showing as missing", + search_data->title); + app = create_missing_app (search_data); + gs_app_list_add (list, app); + } + + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + g_debug ("%s\n\n", gs_app_to_string (app)); + gs_extras_page_add_app (self, app, list, search_data); + } + + self->pending_search_cnt--; + + /* have all searches finished? */ + if (self->pending_search_cnt == 0) + show_search_results (self); +} + +static void +gs_extras_page_load (GsExtrasPage *self, GPtrArray *array_search_data) +{ + guint i; + + /* cancel any pending searches */ + g_cancellable_cancel (self->search_cancellable); + g_clear_object (&self->search_cancellable); + self->search_cancellable = g_cancellable_new (); + + if (array_search_data != NULL) { + if (self->array_search_data != NULL) + g_ptr_array_unref (self->array_search_data); + self->array_search_data = g_ptr_array_ref (array_search_data); + } + + self->pending_search_cnt = 0; + + /* remove old entries */ + gs_container_remove_all (GTK_CONTAINER (self->list_box_results)); + + /* set state as loading */ + self->state = GS_EXTRAS_PAGE_STATE_LOADING; + + /* start new searches, separate one for each codec */ + for (i = 0; i < self->array_search_data->len; i++) { + GsPluginRefineFlags refine_flags; + SearchData *search_data; + + refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES; + + search_data = g_ptr_array_index (self->array_search_data, i); + if (search_data->search_filename != NULL) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_FILES, + "search", search_data->search_filename, + "refine-flags", refine_flags, + NULL); + g_debug ("searching filename: '%s'", search_data->search_filename); + gs_plugin_loader_job_process_async (self->plugin_loader, + plugin_job, + self->search_cancellable, + search_files_cb, + search_data); + } else if (search_data->package_filename != NULL) { + g_autoptr (GFile) file = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + file = g_file_new_for_path (search_data->package_filename); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", refine_flags, + NULL); + g_debug ("resolving filename to app: '%s'", search_data->package_filename); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->search_cancellable, + file_to_app_cb, + search_data); + } else { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_debug ("searching what provides: '%s'", search_data->search); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES, + "search", search_data->search, + "refine-flags", refine_flags, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->search_cancellable, + get_search_what_provides_cb, + search_data); + } + self->pending_search_cnt++; + } +} + +static void +gs_extras_page_reload (GsPage *page) +{ + GsExtrasPage *self = GS_EXTRAS_PAGE (page); + if (self->array_search_data != NULL) + gs_extras_page_load (self, NULL); +} + +static void +gs_extras_page_search_package_files (GsExtrasPage *self, gchar **files) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; files[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup (files[i]); + search_data->package_filename = g_strdup (files[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_provide_files (GsExtrasPage *self, gchar **files) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; files[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup (files[i]); + search_data->search_filename = g_strdup (files[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_package_names (GsExtrasPage *self, gchar **package_names) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; package_names[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup (package_names[i]); + search_data->search = g_strdup (package_names[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_mime_types (GsExtrasPage *self, gchar **mime_types) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; mime_types[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup_printf (_("%s file format"), mime_types[i]); + search_data->search = g_strdup (mime_types[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_MIME); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static gchar * +font_tag_to_lang (const gchar *tag) +{ + if (g_str_has_prefix (tag, ":lang=")) + return g_strdup (tag + 6); + + return NULL; +} + +static gchar * +gs_extras_page_font_tag_to_localised_name (GsExtrasPage *self, const gchar *tag) +{ + gchar *name; + g_autofree gchar *lang = NULL; + g_autofree gchar *language = NULL; + + /* use fontconfig to get the language code */ + lang = font_tag_to_lang (tag); + if (lang == NULL) { + g_warning ("Could not parse language tag '%s'", tag); + return NULL; + } + + /* convert to localisable name */ + language = gs_language_iso639_to_language (self->language, lang); + if (language == NULL) { + g_warning ("Could not match language code '%s' to an ISO639 language", lang); + return NULL; + } + + /* get translation, or return untranslated string */ + name = g_strdup (dgettext("iso_639", language)); + if (name == NULL) + name = g_strdup (language); + + return name; +} + +static void +gs_extras_page_search_fontconfig_resources (GsExtrasPage *self, gchar **resources) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; resources[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = gs_extras_page_font_tag_to_localised_name (self, resources[i]); + search_data->search = g_strdup (resources[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_FONT); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_gstreamer_resources (GsExtrasPage *self, gchar **resources) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; resources[i] != NULL; i++) { + SearchData *search_data; + g_auto(GStrv) parts = NULL; + + parts = g_strsplit (resources[i], "|", 2); + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup (parts[0]); + search_data->search = g_strdup (parts[1]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_CODEC); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_plasma_resources (GsExtrasPage *self, gchar **resources) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i; + + for (i = 0; resources[i] != NULL; i++) { + SearchData *search_data; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup (resources[i]); + search_data->search = g_strdup (resources[i]); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_DEFAULT); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +static void +gs_extras_page_search_printer_drivers (GsExtrasPage *self, gchar **device_ids) +{ + g_autoptr(GPtrArray) array_search_data = g_ptr_array_new_with_free_func ((GDestroyNotify) search_data_free); + guint i, j; + guint len; + + len = g_strv_length (device_ids); + if (len > 1) + /* hardcode for now as we only support one at a time */ + len = 1; + + /* make a list of provides tags */ + for (i = 0; i < len; i++) { + SearchData *search_data; + gchar *p; + guint n_fields; + g_autofree gchar *tag = NULL; + g_autofree gchar *mfg = NULL; + g_autofree gchar *mdl = NULL; + g_auto(GStrv) fields = NULL; + + fields = g_strsplit (device_ids[i], ";", 0); + n_fields = g_strv_length (fields); + mfg = mdl = NULL; + for (j = 0; j < n_fields && (!mfg || !mdl); j++) { + if (g_str_has_prefix (fields[j], "MFG:")) + mfg = g_strdup (fields[j] + 4); + else if (g_str_has_prefix (fields[j], "MDL:")) + mdl = g_strdup (fields[j] + 4); + } + + if (!mfg || !mdl) { + g_warning("invalid line '%s', missing field", + device_ids[i]); + continue; + } + + tag = g_strdup_printf ("%s;%s;", mfg, mdl); + + /* Replace spaces with underscores */ + for (p = tag; *p != '\0'; p++) + if (*p == ' ') + *p = '_'; + + search_data = g_slice_new0 (SearchData); + search_data->title = g_strdup_printf ("%s %s", mfg, mdl); + search_data->search = g_ascii_strdown (tag, -1); + search_data->url_not_found = gs_vendor_get_not_found_url (self->vendor, GS_VENDOR_URL_TYPE_HARDWARE); + search_data->self = g_object_ref (self); + g_ptr_array_add (array_search_data, search_data); + } + + gs_extras_page_load (self, array_search_data); +} + +void +gs_extras_page_search (GsExtrasPage *self, + const gchar *mode_str, + gchar **resources) +{ + self->mode = gs_extras_page_mode_from_string (mode_str); + switch (self->mode) { + case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES: + gs_extras_page_search_package_files (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES: + gs_extras_page_search_provide_files (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES: + gs_extras_page_search_package_names (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES: + gs_extras_page_search_mime_types (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES: + gs_extras_page_search_fontconfig_resources (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES: + gs_extras_page_search_gstreamer_resources (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES: + gs_extras_page_search_plasma_resources (self, resources); + break; + case GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS: + gs_extras_page_search_printer_drivers (self, resources); + break; + default: + g_assert_not_reached (); + break; + } +} + +static void +gs_extras_page_switch_to (GsPage *page, + gboolean scroll_up) +{ + GsExtrasPage *self = GS_EXTRAS_PAGE (page); + GtkWidget *widget; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_EXTRAS) { + g_warning ("Called switch_to(codecs) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "application_details_header")); + gtk_widget_show (widget); + + if (scroll_up) { + GtkAdjustment *adj; + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } + + gs_extras_page_update_ui_state (self); +} + +static void +row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsExtrasPage *self) +{ + GsApp *app; + + app = gs_app_row_get_app (GS_APP_ROW (row)); + + if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE && + gs_app_get_url (app, AS_URL_KIND_MISSING) != NULL) { + gs_shell_show_uri (self->shell, + gs_app_get_url (app, AS_URL_KIND_MISSING)); + } else { + gs_shell_show_app (self->shell, app); + } +} + +static gchar * +get_app_sort_key (GsApp *app) +{ + GString *key = NULL; + g_autofree gchar *sort_name = NULL; + + key = g_string_sized_new (64); + + /* sort missing applications as last */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_UNAVAILABLE: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* finally, sort by short name */ + if (gs_app_get_name (app) != NULL) { + sort_name = gs_utils_sort_key (gs_app_get_name (app)); + g_string_append (key, sort_name); + } + + return g_string_free (key, FALSE); +} + +static gint +list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a)); + GsApp *a2 = gs_app_row_get_app (GS_APP_ROW (b)); + g_autofree gchar *key1 = get_app_sort_key (a1); + g_autofree gchar *key2 = get_app_sort_key (a2); + + /* compare the keys according to the algorithm above */ + return g_strcmp0 (key1, key2); +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header; + + /* first entry */ + header = gtk_list_box_row_get_header (row); + if (before == NULL) { + gtk_list_box_row_set_header (row, NULL); + return; + } + + /* already set */ + if (header != NULL) + return; + + /* set new */ + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static gboolean +gs_extras_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsExtrasPage *self = GS_EXTRAS_PAGE (page); + + g_return_val_if_fail (GS_IS_EXTRAS_PAGE (self), TRUE); + + self->shell = shell; + + self->plugin_loader = g_object_ref (plugin_loader); + self->builder = g_object_ref (builder); + + g_signal_connect (self->list_box_results, "row-activated", + G_CALLBACK (row_activated_cb), self); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_results), + list_header_func, + self, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_results), + list_sort_func, + self, NULL); + return TRUE; +} + +static void +gs_extras_page_dispose (GObject *object) +{ + GsExtrasPage *self = GS_EXTRAS_PAGE (object); + + g_cancellable_cancel (self->search_cancellable); + g_clear_object (&self->search_cancellable); + + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + g_clear_object (&self->language); + g_clear_object (&self->vendor); + g_clear_object (&self->builder); + g_clear_object (&self->plugin_loader); + + g_clear_pointer (&self->array_search_data, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_extras_page_parent_class)->dispose (object); +} + +static void +gs_extras_page_init (GsExtrasPage *self) +{ + g_autoptr(GError) error = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->state = GS_EXTRAS_PAGE_STATE_LOADING; + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->vendor = gs_vendor_new (); + + /* map ISO639 to language names */ + self->language = gs_language_new (); + gs_language_populate (self->language, &error); + if (error != NULL) + g_error ("Failed to map ISO639 to language names: %s", error->message); +} + +static void +gs_extras_page_class_init (GsExtrasPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_extras_page_dispose; + page_class->switch_to = gs_extras_page_switch_to; + page_class->reload = gs_extras_page_reload; + page_class->setup = gs_extras_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-extras-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_failed); + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, label_no_results); + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, list_box_results); + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, scrolledwindow); + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, spinner); + gtk_widget_class_bind_template_child (widget_class, GsExtrasPage, stack); +} + +GsExtrasPage * +gs_extras_page_new (void) +{ + GsExtrasPage *self; + self = g_object_new (GS_TYPE_EXTRAS_PAGE, NULL); + return GS_EXTRAS_PAGE (self); +} diff --git a/src/gs-extras-page.h b/src/gs-extras-page.h new file mode 100644 index 0000000..e4dc001 --- /dev/null +++ b/src/gs-extras-page.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_EXTRAS_PAGE (gs_extras_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsExtrasPage, gs_extras_page, GS, EXTRAS_PAGE, GsPage) + +typedef enum { + GS_EXTRAS_PAGE_MODE_UNKNOWN, + GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_FILES, + GS_EXTRAS_PAGE_MODE_INSTALL_PROVIDE_FILES, + GS_EXTRAS_PAGE_MODE_INSTALL_PACKAGE_NAMES, + GS_EXTRAS_PAGE_MODE_INSTALL_MIME_TYPES, + GS_EXTRAS_PAGE_MODE_INSTALL_FONTCONFIG_RESOURCES, + GS_EXTRAS_PAGE_MODE_INSTALL_GSTREAMER_RESOURCES, + GS_EXTRAS_PAGE_MODE_INSTALL_PLASMA_RESOURCES, + GS_EXTRAS_PAGE_MODE_INSTALL_PRINTER_DRIVERS, + GS_EXTRAS_PAGE_MODE_LAST +} GsExtrasPageMode; + +const gchar *gs_extras_page_mode_to_string (GsExtrasPageMode mode); +GsExtrasPage *gs_extras_page_new (void); +void gs_extras_page_search (GsExtrasPage *self, + const gchar *mode, + gchar **resources); + +G_END_DECLS diff --git a/src/gs-extras-page.ui b/src/gs-extras-page.ui new file mode 100644 index 0000000..7d61be8 --- /dev/null +++ b/src/gs-extras-page.ui @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsExtrasPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="codecs-accessible"> + <property name="accessible-name" translatable="yes">Codecs page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box_spinner"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkSpinner" id="spinner"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + </child> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <child> + <object class="GtkBox" id="box_results"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkListBox" id="list_box_results"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + <child> + <object class="GtkSeparator" id="separator_results"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">results</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_no_results"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">24</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="image_no_results"> + <property name="visible">True</property> + <property name="pixel_size">64</property> + <property name="icon_name">face-sad-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_no_results"> + <property name="visible">True</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">60</property> + </object> + </child> + </object> + <packing> + <property name="name">no-results</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_failed"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_failed"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">action-unavailable-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_failed"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="name">failed</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-feature-tile.c b/src/gs-feature-tile.c new file mode 100644 index 0000000..9cc5f7c --- /dev/null +++ b/src/gs-feature-tile.c @@ -0,0 +1,138 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "gs-feature-tile.h" +#include "gs-common.h" +#include "gs-css.h" + +struct _GsFeatureTile +{ + GsAppTile parent_instance; + GtkWidget *stack; + GtkWidget *title; + GtkWidget *subtitle; + const gchar *markup_cache; /* (unowned) (nullable) */ + GtkCssProvider *tile_provider; /* (owned) (nullable) */ + GtkCssProvider *title_provider; /* (owned) (nullable) */ + GtkCssProvider *subtitle_provider; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsFeatureTile, gs_feature_tile, GS_TYPE_APP_TILE) + +static void +gs_feature_tile_dispose (GObject *object) +{ + GsFeatureTile *tile = GS_FEATURE_TILE (object); + + g_clear_object (&tile->tile_provider); + g_clear_object (&tile->title_provider); + g_clear_object (&tile->subtitle_provider); + + G_OBJECT_CLASS (gs_feature_tile_parent_class)->dispose (object); +} + +static void +gs_feature_tile_refresh (GsAppTile *self) +{ + GsFeatureTile *tile = GS_FEATURE_TILE (self); + GsApp *app = gs_app_tile_get_app (self); + AtkObject *accessible; + const gchar *markup; + g_autofree gchar *name = NULL; + + if (app == NULL) + return; + + gtk_stack_set_visible_child_name (GTK_STACK (tile->stack), "content"); + + /* update text */ + gtk_label_set_label (GTK_LABEL (tile->title), gs_app_get_name (app)); + gtk_label_set_label (GTK_LABEL (tile->subtitle), gs_app_get_summary (app)); + + /* perhaps set custom css; cache it so that images don’t get reloaded + * unnecessarily */ + markup = gs_app_get_metadata_item (app, "GnomeSoftware::FeatureTile-css"); + if (tile->markup_cache != markup) { + g_autoptr(GsCss) css = gs_css_new (); + if (markup != NULL) + gs_css_parse (css, markup, NULL); + gs_utils_widget_set_css (GTK_WIDGET (tile), &tile->tile_provider, "feature-tile", + gs_css_get_markup_for_id (css, "tile")); + gs_utils_widget_set_css (tile->title, &tile->title_provider, "feature-tile-name", + gs_css_get_markup_for_id (css, "name")); + gs_utils_widget_set_css (tile->subtitle, &tile->subtitle_provider, "feature-tile-subtitle", + gs_css_get_markup_for_id (css, "summary")); + tile->markup_cache = markup; + } + + accessible = gtk_widget_get_accessible (GTK_WIDGET (tile)); + + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + name = g_strdup_printf ("%s (%s)", + gs_app_get_name (app), + _("Installed")); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_INSTALLING: + default: + name = g_strdup (gs_app_get_name (app)); + break; + } + + if (GTK_IS_ACCESSIBLE (accessible) && name != NULL) { + atk_object_set_name (accessible, name); + atk_object_set_description (accessible, gs_app_get_summary (app)); + } +} + +static void +gs_feature_tile_init (GsFeatureTile *tile) +{ + gtk_widget_set_has_window (GTK_WIDGET (tile), FALSE); + gtk_widget_init_template (GTK_WIDGET (tile)); +} + +static void +gs_feature_tile_class_init (GsFeatureTileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GsAppTileClass *app_tile_class = GS_APP_TILE_CLASS (klass); + + object_class->dispose = gs_feature_tile_dispose; + + app_tile_class->refresh = gs_feature_tile_refresh; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-feature-tile.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, stack); + gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, title); + gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, subtitle); +} + +GtkWidget * +gs_feature_tile_new (GsApp *app) +{ + GsFeatureTile *tile; + tile = g_object_new (GS_TYPE_FEATURE_TILE, + "vexpand", FALSE, + NULL); + if (app != NULL) + gs_app_tile_set_app (GS_APP_TILE (tile), app); + return GTK_WIDGET (tile); +} diff --git a/src/gs-feature-tile.h b/src/gs-feature-tile.h new file mode 100644 index 0000000..e9b65ea --- /dev/null +++ b/src/gs-feature-tile.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app-tile.h" + +G_BEGIN_DECLS + +#define GS_TYPE_FEATURE_TILE (gs_feature_tile_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFeatureTile, gs_feature_tile, GS, FEATURE_TILE, GsAppTile) + +GtkWidget *gs_feature_tile_new (GsApp *app); + +G_END_DECLS diff --git a/src/gs-feature-tile.ui b/src/gs-feature-tile.ui new file mode 100644 index 0000000..fae458e --- /dev/null +++ b/src/gs-feature-tile.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsFeatureTile" parent="GsAppTile"> + <property name="visible">True</property> + <property name="halign">fill</property> + <style> + <class name="featured-tile"/> + </style> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="waiting"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="icon-name">content-loading-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="name">waiting</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkBox" id="box2"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="orientation">vertical</property> + <property name="margin">50</property> + <child> + <object class="GtkLabel" id="title"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="halign">center</property> + <property name="valign">end</property> + <property name="vexpand">True</property> + <property name="ellipsize">end</property> + <style> + <class name="title-1"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="subtitle"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0</property> + <property name="halign">center</property> + <property name="valign">start</property> + <style> + <class name="caption"/> + </style> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">content</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-first-run-dialog.c b/src/gs-first-run-dialog.c new file mode 100644 index 0000000..5603479 --- /dev/null +++ b/src/gs-first-run-dialog.c @@ -0,0 +1,60 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gtk/gtk.h> + +#include "gs-first-run-dialog.h" + +struct _GsFirstRunDialog +{ + GtkDialog parent_instance; + + GtkWidget *button; +}; + +G_DEFINE_TYPE (GsFirstRunDialog, gs_first_run_dialog, GTK_TYPE_DIALOG) + +static void +button_clicked_cb (GtkWidget *widget, GsFirstRunDialog *dialog) +{ + gtk_window_close (GTK_WINDOW (dialog)); +} + +static void +gs_first_run_dialog_init (GsFirstRunDialog *dialog) +{ + GtkWidget *button_label; + + gtk_widget_init_template (GTK_WIDGET (dialog)); + + button_label = gtk_bin_get_child (GTK_BIN (dialog->button)); + gtk_widget_set_margin_start (button_label, 16); + gtk_widget_set_margin_end (button_label, 16); + + g_signal_connect (dialog->button, "clicked", G_CALLBACK (button_clicked_cb), dialog); +} + +static void +gs_first_run_dialog_class_init (GsFirstRunDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-first-run-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsFirstRunDialog, button); +} + +GtkWidget * +gs_first_run_dialog_new (void) +{ + return GTK_WIDGET (g_object_new (GS_TYPE_FIRST_RUN_DIALOG, + "use-header-bar", TRUE, + NULL)); +} diff --git a/src/gs-first-run-dialog.h b/src/gs-first-run-dialog.h new file mode 100644 index 0000000..0419584 --- /dev/null +++ b/src/gs-first-run-dialog.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FIRST_RUN_DIALOG (gs_first_run_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFirstRunDialog, gs_first_run_dialog, GS, FIRST_RUN_DIALOG, GtkDialog) + +GtkWidget *gs_first_run_dialog_new (void); + +G_END_DECLS diff --git a/src/gs-first-run-dialog.ui b/src/gs-first-run-dialog.ui new file mode 100644 index 0000000..3861bd8 --- /dev/null +++ b/src/gs-first-run-dialog.ui @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.18.1 --> +<interface> + <requires lib="gtk+" version="3.12"/> + <template class="GsFirstRunDialog" parent="GtkDialog"> + <property name="title" translatable="yes">Welcome</property> + <property name="modal">True</property> + <property name="resizable">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <property name="title" translatable="yes">Welcome</property> + <property name="show_close_button">False</property> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="margin_start">55</property> + <property name="margin_end">55</property> + <property name="margin_top">44</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="box_empty"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">16</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">org.gnome.Software</property> + <style> + <class name="icon-dropshadow"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Welcome to Software</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.7"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes">Software lets you install all the software you need, all from one place. See our recommendations, browse the categories, or search for the applications you want.</property> + <property name="wrap">True</property> + <property name="max_width_chars">48</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="button"> + <property name="label" translatable="yes">_Let’s Go Shopping</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin_top">23</property> + <property name="margin_bottom">26</property> + <property name="use_underline">True</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-fixed-size-bin.c b/src/gs-fixed-size-bin.c new file mode 100644 index 0000000..7d326ee --- /dev/null +++ b/src/gs-fixed-size-bin.c @@ -0,0 +1,237 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-fixed-size-bin.h" + +struct _GsFixedSizeBin { + GtkBin parent; + gint preferred_width; + gint preferred_height; + gint min_width; + gint min_height; +}; + +G_DEFINE_TYPE (GsFixedSizeBin, gs_fixed_size_bin, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_PREFERRED_WIDTH, + PROP_PREFERRED_HEIGHT +}; + +static void +gs_fixed_size_bin_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (widget); + + if (bin->preferred_width >= 0 && + bin->preferred_width >= bin->min_width && + allocation->width > bin->preferred_width) { + /* Center the contents */ + allocation->x += (allocation->width - bin->preferred_width) / 2; + allocation->width = bin->preferred_width; + } + if (bin->preferred_height >= 0 && + bin->preferred_height >= bin->min_height && + allocation->height > bin->preferred_height) { + /* Align to the top */ + allocation->height = bin->preferred_height; + } + + GTK_WIDGET_CLASS (gs_fixed_size_bin_parent_class)->size_allocate (widget, allocation); +} + +static void +gs_fixed_size_bin_get_preferred_width (GtkWidget *widget, + gint *min, gint *nat) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (widget); + gint m, n; + + GTK_WIDGET_CLASS (gs_fixed_size_bin_parent_class)->get_preferred_width (widget, &m, &n); + + bin->min_width = m; + if (bin->preferred_width >= 0 && n > bin->preferred_width) + n = MAX (m, bin->preferred_width); + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_fixed_size_bin_get_preferred_height (GtkWidget *widget, + gint *min, gint *nat) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (widget); + gint m, n; + + GTK_WIDGET_CLASS (gs_fixed_size_bin_parent_class)->get_preferred_height (widget, &m, &n); + + bin->min_height = m; + if (bin->preferred_height >= 0 && n > bin->preferred_height) + n = MAX (m, bin->preferred_height); + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_fixed_size_bin_get_preferred_width_for_height (GtkWidget *widget, + gint for_height, + gint *min, gint *nat) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (widget); + gint m, n; + + if (gtk_widget_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) { + GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, + min, nat); + return; + } + + if (bin->preferred_height >= 0 && + for_height > bin->preferred_height) { + /* The height will be limited */ + for_height = MAX (bin->min_height, bin->preferred_height); + } + + GTK_WIDGET_CLASS (gs_fixed_size_bin_parent_class)->get_preferred_width_for_height ( + widget, for_height, &m, &n); + + bin->min_width = m; + if (bin->preferred_width >= 0 && n > bin->preferred_width) + n = MAX (m, bin->preferred_width); + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_fixed_size_bin_get_preferred_height_for_width (GtkWidget *widget, + gint for_width, + gint *min, gint *nat) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (widget); + gint m, n; + + if (gtk_widget_get_request_mode (widget) == GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT) { + GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, + min, nat); + return; + } + + if (bin->preferred_width >= 0 && + for_width > bin->preferred_width) { + /* The width will be limited */ + for_width = MAX (bin->min_width, bin->preferred_width); + } + + GTK_WIDGET_CLASS (gs_fixed_size_bin_parent_class)->get_preferred_height_for_width ( + widget, for_width, &m, &n); + + bin->min_height = m; + if (bin->preferred_height >= 0 && n > bin->preferred_height) + n = MAX (m, bin->preferred_height); + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_fixed_size_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (object); + + switch (prop_id) { + case PROP_PREFERRED_WIDTH: + g_value_set_int (value, bin->preferred_width); + break; + case PROP_PREFERRED_HEIGHT: + g_value_set_int (value, bin->preferred_height); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_fixed_size_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsFixedSizeBin *bin = GS_FIXED_SIZE_BIN (object); + + switch (prop_id) { + case PROP_PREFERRED_WIDTH: + bin->preferred_width = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (bin)); + break; + case PROP_PREFERRED_HEIGHT: + bin->preferred_height = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (bin)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_fixed_size_bin_init (GsFixedSizeBin *bin) +{ + gtk_widget_set_has_window (GTK_WIDGET (bin), FALSE); + bin->preferred_width = -1; + bin->preferred_height = -1; +} + +static void +gs_fixed_size_bin_class_init (GsFixedSizeBinClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->get_property = gs_fixed_size_bin_get_property; + object_class->set_property = gs_fixed_size_bin_set_property; + + widget_class->size_allocate = gs_fixed_size_bin_size_allocate; + widget_class->get_preferred_width = gs_fixed_size_bin_get_preferred_width; + widget_class->get_preferred_height = gs_fixed_size_bin_get_preferred_height; + widget_class->get_preferred_width_for_height = gs_fixed_size_bin_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = gs_fixed_size_bin_get_preferred_height_for_width; + + g_object_class_install_property (object_class, PROP_PREFERRED_WIDTH, + g_param_spec_int ("preferred-width", + "Preferred width", + "The width of this widget unless its parent is smaller or its child requires more", + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_PREFERRED_HEIGHT, + g_param_spec_int ("preferred-height", + "Preferred height", + "The height of this widget unless its parent is smaller or its child requires more", + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); +} + +GtkWidget * +gs_fixed_size_bin_new (void) +{ + return g_object_new (GS_TYPE_FIXED_SIZE_BIN, NULL); +} diff --git a/src/gs-fixed-size-bin.h b/src/gs-fixed-size-bin.h new file mode 100644 index 0000000..e28edc9 --- /dev/null +++ b/src/gs-fixed-size-bin.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FIXED_SIZE_BIN (gs_fixed_size_bin_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFixedSizeBin, gs_fixed_size_bin, GS, FIXED_SIZE_BIN, GtkBin) + +GtkWidget *gs_fixed_size_bin_new (void); + +G_END_DECLS diff --git a/src/gs-folders.c b/src/gs-folders.c new file mode 100644 index 0000000..295ec39 --- /dev/null +++ b/src/gs-folders.c @@ -0,0 +1,610 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "gs-folders.h" + +#define APP_FOLDER_SCHEMA "org.gnome.desktop.app-folders" +#define APP_FOLDER_CHILD_SCHEMA "org.gnome.desktop.app-folders.folder" + +/* We are loading folders from a settings with type + * a{sas}, which maps folder ids to list of app ids. + * + * For convenience, we unfold this variant into GsFolder + * structs and two hash tables, one with folder ids + * as keys, and one with app ids. + */ + +typedef struct +{ + gchar *id; + gchar *name; + gchar *translated; + gboolean translate; + GHashTable *apps; + GHashTable *categories; + GHashTable *excluded_apps; +} GsFolder; + +struct _GsFolders +{ + GObject parent_instance; + + GSettings *settings; + GHashTable *folders; + GHashTable *apps; + GHashTable *categories; +}; + +G_DEFINE_TYPE (GsFolders, gs_folders, G_TYPE_OBJECT) + +#if 0 +static void +dump_set (GHashTable *set) +{ + GHashTableIter iter; + const gchar *key; + + g_hash_table_iter_init (&iter, set); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL)) { + g_print ("\t\t%s\n", key); + } +} + +static void +dump_map (GHashTable *map) +{ + GHashTableIter iter; + const gchar *key; + GsFolder *folder; + + g_hash_table_iter_init (&iter, map); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&folder)) { + g_print ("\t%s -> %s\n", key, folder->id); + } +} + +static void +dump (GsFolders *folders) +{ + GHashTableIter iter; + GsFolder *folder; + + g_hash_table_iter_init (&iter, folders->folders); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&folder)) { + g_print ("folder %s\n", folder->id); + g_print ("\tname %s\n", folder->name); + g_print ("\ttranslate %d\n", folder->translate); + if (g_hash_table_size (folder->apps) > 0) { + g_print ("\tapps\n"); + dump_set (folder->apps); + } + if (g_hash_table_size (folder->categories) > 0) { + g_print ("\tcategories\n"); + dump_set (folder->categories); + } + if (g_hash_table_size (folder->excluded_apps) > 0) { + g_print ("\texcluded\n"); + dump_set (folder->excluded_apps); + } + } + + g_print ("app mapping\n"); + dump_map (folders->apps); + g_print ("category mapping\n"); + dump_map (folders->categories); +} +#endif + +static gchar * +lookup_folder_name (const gchar *id) +{ + gchar *name = NULL; + g_autofree gchar *file = NULL; + g_autoptr(GKeyFile) key_file = NULL; + + file = g_build_filename ("desktop-directories", id, NULL); + key_file = g_key_file_new (); + if (g_key_file_load_from_data_dirs (key_file, file, NULL, G_KEY_FILE_NONE, NULL)) { + name = g_key_file_get_locale_string (key_file, "Desktop Entry", "Name", NULL, NULL); + } + return name; +} + +static GsFolder * +gs_folder_new (const gchar *id, const gchar *name, gboolean translate) +{ + GsFolder *folder; + + folder = g_new0 (GsFolder, 1); + folder->id = g_strdup (id); + folder->name = g_strdup (name); + folder->translate = translate; + if (translate) { + folder->translated = lookup_folder_name (name); + } + folder->apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + folder->categories = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + folder->excluded_apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + return folder; +} + +static void +gs_folder_free (GsFolder *folder) +{ + g_free (folder->id); + g_free (folder->name); + g_free (folder->translated); + g_hash_table_destroy (folder->apps); + g_hash_table_destroy (folder->categories); + g_hash_table_destroy (folder->excluded_apps); + g_free (folder); +} + +static void +load (GsFolders *folders) +{ + GsFolder *folder; + guint i, j; + gboolean translate; + GHashTableIter iter; + gchar *app; + gchar *category; + g_autofree gchar *path = NULL; + g_auto(GStrv) ids = NULL; + + folders->folders = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)gs_folder_free); + folders->apps = g_hash_table_new (g_str_hash, g_str_equal); + folders->categories = g_hash_table_new (g_str_hash, g_str_equal); + + ids = g_settings_get_strv (folders->settings, "folder-children"); + g_object_get (folders->settings, "path", &path, NULL); + for (i = 0; ids[i]; i++) { + g_auto(GStrv) apps = NULL; + g_auto(GStrv) categories = NULL; + g_autofree gchar *child_path = NULL; + g_auto(GStrv) excluded_apps = NULL; + g_autofree gchar *name = NULL; + g_autoptr(GSettings) settings = NULL; + + child_path = g_strconcat (path, "folders/", ids[i], "/", NULL); + settings = g_settings_new_with_path (APP_FOLDER_CHILD_SCHEMA, child_path); + if (settings == NULL) { + g_warning ("ignoring folder child %s as invalid", ids[i]); + continue; + } + name = g_settings_get_string (settings, "name"); + translate = g_settings_get_boolean (settings, "translate"); + folder = gs_folder_new (ids[i], name, translate); + + excluded_apps = g_settings_get_strv (settings, "excluded-apps"); + for (j = 0; excluded_apps[j]; j++) { + g_hash_table_add (folder->excluded_apps, g_strdup (excluded_apps[j])); + } + + apps = g_settings_get_strv (settings, "apps"); + for (j = 0; apps[j]; j++) { + if (!g_hash_table_contains (folder->excluded_apps, apps[j])) + g_hash_table_add (folder->apps, g_strdup (apps[j])); + } + + categories = g_settings_get_strv (settings, "categories"); + for (j = 0; categories[j]; j++) { + g_hash_table_add (folder->categories, g_strdup (categories[j])); + } + + g_hash_table_insert (folders->folders, (gpointer)folder->id, folder); + g_hash_table_iter_init (&iter, folder->apps); + while (g_hash_table_iter_next (&iter, (gpointer*)&app, NULL)) { + g_hash_table_insert (folders->apps, app, folder); + } + + g_hash_table_iter_init (&iter, folder->categories); + while (g_hash_table_iter_next (&iter, (gpointer*)&category, NULL)) { + g_hash_table_insert (folders->categories, category, folder); + } + } +} + +static void +save (GsFolders *folders) +{ + GHashTableIter iter; + GsFolder *folder; + gpointer keys; + g_autofree gchar *path = NULL; + g_autofree gpointer apps = NULL; + + g_object_get (folders->settings, "path", &path, NULL); + g_hash_table_iter_init (&iter, folders->folders); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&folder)) { + g_autofree gchar *child_path = NULL; + g_autoptr(GSettings) settings = NULL; + + child_path = g_strconcat (path, "folders/", folder->id, "/", NULL); + settings = g_settings_new_with_path (APP_FOLDER_CHILD_SCHEMA, child_path); + g_settings_set_string (settings, "name", folder->name); + g_settings_set_boolean (settings, "translate", folder->translate); + keys = g_hash_table_get_keys_as_array (folder->apps, NULL); + g_settings_set_strv (settings, "apps", (const gchar * const *)keys); + g_free (keys); + + keys = g_hash_table_get_keys_as_array (folder->excluded_apps, NULL); + g_settings_set_strv (settings, "excluded-apps", (const gchar * const *)keys); + g_free (keys); + + keys = g_hash_table_get_keys_as_array (folder->categories, NULL); + g_settings_set_strv (settings, "categories", (const gchar * const *)keys); + g_free (keys); + } + + apps = gs_folders_get_nonempty_folders (folders); + g_settings_set_strv (folders->settings, "folder-children", + (const gchar * const *)apps); +} + +static void +clear (GsFolders *folders) +{ + g_hash_table_unref (folders->apps); + g_hash_table_unref (folders->categories); + g_hash_table_unref (folders->folders); + + folders->apps = NULL; + folders->categories = NULL; + folders->folders = NULL; +} + +static void +gs_folders_dispose (GObject *object) +{ + GsFolders *folders = GS_FOLDERS (object); + + g_clear_object (&folders->settings); + + G_OBJECT_CLASS (gs_folders_parent_class)->dispose (object); +} + +static void +gs_folders_finalize (GObject *object) +{ + GsFolders *folders = GS_FOLDERS (object); + + clear (folders); + + G_OBJECT_CLASS (gs_folders_parent_class)->finalize (object); +} + +static void +gs_folders_class_init (GsFoldersClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_folders_dispose; + object_class->finalize = gs_folders_finalize; +} + +static void +gs_folders_init (GsFolders *folders) +{ + folders->settings = g_settings_new (APP_FOLDER_SCHEMA); + load (folders); +} + +static GsFolders * +gs_folders_new (void) +{ + return GS_FOLDERS (g_object_new (GS_TYPE_FOLDERS, NULL)); +} + +static GsFolders *singleton; + +GsFolders * +gs_folders_get (void) +{ + if (!singleton) + singleton = gs_folders_new (); + + return g_object_ref (singleton); +} + +gchar ** +gs_folders_get_folders (GsFolders *folders) +{ + return (gchar**) g_hash_table_get_keys_as_array (folders->folders, NULL); +} + +gchar ** +gs_folders_get_nonempty_folders (GsFolders *folders) +{ + GHashTableIter iter; + GsFolder *folder; + g_autoptr(GHashTable) tmp = NULL; + + tmp = g_hash_table_new (g_str_hash, g_str_equal); + + g_hash_table_iter_init (&iter, folders->apps); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&folder)) { + g_hash_table_add (tmp, folder->id); + } + + g_hash_table_iter_init (&iter, folders->categories); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&folder)) { + g_hash_table_add (tmp, folder->id); + } + + return (gchar **) g_hash_table_get_keys_as_array (tmp, NULL); +} + +static void +canonicalize_key (gchar *key) +{ + gchar *p; + + for (p = key; *p != 0; p++) { + gchar c = *p; + + if (c != '-' && + (c < '0' || c > '9') && + (c < 'A' || c > 'Z') && + (c < 'a' || c > 'z')) + *p = '-'; + } +} + +const gchar * +gs_folders_add_folder (GsFolders *folders, const gchar *id) +{ + GsFolder *folder; + g_autofree gchar *key = NULL; + + key = g_strdup (id); + canonicalize_key (key); + folder = g_hash_table_lookup (folders->folders, key); + if (!folder) { + folder = gs_folder_new (key, id, FALSE); + g_hash_table_insert (folders->folders, folder->id, folder); + } + + return folder->id; +} + +void +gs_folders_remove_folder (GsFolders *folders, const gchar *id) +{ + GsFolder *folder = NULL; + GHashTableIter iter; + + if (id == NULL) + return; + + g_hash_table_iter_init (&iter, folders->apps); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*)&folder)) { + if (folder && g_strcmp0 (id, folder->id) == 0) { + g_hash_table_iter_remove (&iter); + } + } + + g_hash_table_iter_init (&iter, folders->categories); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*)&folder)) { + if (folder && g_strcmp0 (id, folder->id) == 0) { + g_hash_table_iter_remove (&iter); + } + } + + if (folder != NULL) + g_hash_table_remove (folders->folders, folder->id); +} + +const gchar * +gs_folders_get_folder_name (GsFolders *folders, const gchar *id) +{ + GsFolder *folder; + + folder = g_hash_table_lookup (folders->folders, id); + + if (folder) { + if (folder->translated) + return folder->translated; + + return folder->name; + } + + return NULL; +} + +void +gs_folders_set_folder_name (GsFolders *folders, const gchar *id, const gchar *name) +{ + GsFolder *folder; + + folder = g_hash_table_lookup (folders->folders, id); + if (folder) { + g_free (folder->name); + g_free (folder->translated); + folder->name = g_strdup (name); + folder->translate = FALSE; + } +} + +static GsFolder * +get_app_folder (GsFolders *folders, const gchar *app, GPtrArray *categories) +{ + GsFolder *folder; + const gchar *category; + guint i; + + folder = g_hash_table_lookup (folders->apps, app); + if (!folder && categories) { + for (i = 0; i < categories->len; i++) { + category = g_ptr_array_index (categories, i); + if (category == NULL) + continue; + + folder = g_hash_table_lookup (folders->categories, category); + if (folder) { + break; + } + } + } + if (folder) { + if (g_hash_table_contains (folder->excluded_apps, app)) { + folder = NULL; + } + } + + return folder; +} + +const gchar * +gs_folders_get_app_folder (GsFolders *folders, const gchar *app, GPtrArray *categories) +{ + GsFolder *folder; + + if (app == NULL) + return NULL; + + folder = get_app_folder (folders, app, categories); + + return folder ? folder->id : NULL; +} + +void +gs_folders_set_app_folder (GsFolders *folders, const gchar *app, GPtrArray *categories, const gchar *id) +{ + GsFolder *folder; + + folder = get_app_folder (folders, app, categories); + + if (folder) { + g_hash_table_remove (folders->apps, app); + g_hash_table_remove (folder->apps, app); + } + + if (id) { + gchar *app_id; + + app_id = g_strdup (app); + folder = g_hash_table_lookup (folders->folders, id); + g_hash_table_add (folder->apps, app_id); + g_hash_table_remove (folder->excluded_apps, app); + g_hash_table_insert (folders->apps, app_id, folder); + } else { + guint i; + gchar *category; + + for (i = 0; i < categories->len; i++) { + category = g_ptr_array_index (categories, i); + folder = g_hash_table_lookup (folders->categories, category); + if (folder) { + g_hash_table_add (folder->excluded_apps, g_strdup (app)); + } + } + } +} + +void +gs_folders_save (GsFolders *folders) +{ + save (folders); +} + +void +gs_folders_revert (GsFolders *folders) +{ + clear (folders); + load (folders); +} + +/* Ensure we have the default folders for Utilities and YaST. + * We can't do this as default values, since the schemas have + * no fixed path. + * + * The app lists come from gnome-menus: layout/gnome-applications.menu + */ +void +gs_folders_convert (void) +{ + g_autoptr(GSettings) settings = NULL; + g_auto(GStrv) ids = NULL; + + settings = g_settings_new (APP_FOLDER_SCHEMA); + ids = g_settings_get_strv (settings, "folder-children"); + if (g_strv_length (ids) == 0) { + const gchar * const children[] = { + "Utilities", + "YaST", + NULL + }; + const gchar * const utilities_categories[] = { + "X-GNOME-Utilities", + NULL + }; + const gchar * const utilities_apps[] = { + "gnome-abrt.desktop", + "gnome-system-log.desktop", + "gnome-system-monitor.desktop", + "gucharmap.desktop", + "nm-connection-editor.desktop", + "org.gnome.baobab.desktop", + "org.gnome.Calculator.desktop", + "org.gnome.DejaDup.desktop", + "org.gnome.Dictionary.desktop", + "org.gnome.DiskUtility.desktop", + "org.gnome.eog.desktop", + "org.gnome.Evince.desktop", + "org.gnome.FileRoller.desktop", + "org.gnome.fonts.desktop", + "org.gnome.Screenshot.desktop", + "org.gnome.seahorse.Application.desktop", + "org.gnome.Terminal.desktop", + "org.gnome.tweaks.desktop", + "org.gnome.Usage.desktop", + "simple-scan.desktop", + "vinagre.desktop", + "yelp.desktop", + NULL + }; + const gchar * const yast_categories[] = { + "X-SuSE-YaST", + NULL + }; + + gchar *path; + gchar *child_path; + GSettings *child; + + g_settings_set_strv (settings, "folder-children", children); + g_object_get (settings, "path", &path, NULL); + + child_path = g_strconcat (path, "folders/Utilities/", NULL); + child = g_settings_new_with_path (APP_FOLDER_CHILD_SCHEMA, child_path); + g_settings_set_string (child, "name", "X-GNOME-Utilities.directory"); + g_settings_set_boolean (child, "translate", TRUE); + g_settings_set_strv (child, "categories", utilities_categories); + g_settings_set_strv (child, "apps", utilities_apps); + + g_object_unref (child); + g_free (child_path); + + child_path = g_strconcat (path, "folders/YaST/", NULL); + child = g_settings_new_with_path (APP_FOLDER_CHILD_SCHEMA, child_path); + g_settings_set_string (child, "name", "suse-yast.directory"); + g_settings_set_boolean (child, "translate", TRUE); + g_settings_set_strv (child, "categories", yast_categories); + + g_object_unref (child); + g_free (child_path); + + } +} diff --git a/src/gs-folders.h b/src/gs-folders.h new file mode 100644 index 0000000..d23a800 --- /dev/null +++ b/src/gs-folders.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FOLDERS (gs_folders_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFolders, gs_folders, GS, FOLDERS, GObject) + +GsFolders *gs_folders_get (void); + +gchar **gs_folders_get_folders (GsFolders *folders); +gchar **gs_folders_get_nonempty_folders (GsFolders *folders); +const gchar *gs_folders_add_folder (GsFolders *folders, + const gchar *id); +void gs_folders_remove_folder (GsFolders *folders, + const gchar *id); +const gchar *gs_folders_get_folder_name (GsFolders *folders, + const gchar *id); +void gs_folders_set_folder_name (GsFolders *folders, + const gchar *id, + const gchar *name); +const gchar *gs_folders_get_app_folder (GsFolders *folders, + const gchar *app, + GPtrArray *categories); +void gs_folders_set_app_folder (GsFolders *folders, + const gchar *app, + GPtrArray *categories, + const gchar *id); +void gs_folders_save (GsFolders *folders); +void gs_folders_revert (GsFolders *folders); + +void gs_folders_convert (void); + +G_END_DECLS diff --git a/src/gs-hiding-box.c b/src/gs-hiding-box.c new file mode 100644 index 0000000..bada8d8 --- /dev/null +++ b/src/gs-hiding-box.c @@ -0,0 +1,421 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-hiding-box.h" + +enum { + PROP_0, + PROP_SPACING +}; + +struct _GsHidingBox +{ + GtkContainer parent_instance; + + GList *children; + gint spacing; +}; + +static void +gs_hiding_box_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + if (!type) + gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (GS_HIDING_BOX (buildable), type); +} + +static void +gs_hiding_box_buildable_init (GtkBuildableIface *iface) +{ + iface->add_child = gs_hiding_box_buildable_add_child; +} + +G_DEFINE_TYPE_WITH_CODE (GsHidingBox, gs_hiding_box, GTK_TYPE_CONTAINER, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gs_hiding_box_buildable_init)) + +static void +gs_hiding_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsHidingBox *box = GS_HIDING_BOX (object); + + switch (prop_id) { + case PROP_SPACING: + gs_hiding_box_set_spacing (box, g_value_get_int (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_hiding_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsHidingBox *box = GS_HIDING_BOX (object); + + switch (prop_id) { + case PROP_SPACING: + g_value_set_int (value, box->spacing); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_hiding_box_add (GtkContainer *container, GtkWidget *widget) +{ + GsHidingBox *box = GS_HIDING_BOX (container); + + box->children = g_list_append (box->children, widget); + gtk_widget_set_parent (widget, GTK_WIDGET (box)); +} + +static void +gs_hiding_box_remove (GtkContainer *container, GtkWidget *widget) +{ + GList *child; + GsHidingBox *box = GS_HIDING_BOX (container); + + for (child = box->children; child != NULL; child = child->next) { + if (child->data == widget) { + gboolean was_visible = gtk_widget_get_visible (widget) && + gtk_widget_get_child_visible (widget); + + gtk_widget_unparent (widget); + box->children = g_list_delete_link (box->children, child); + + if (was_visible) + gtk_widget_queue_resize (GTK_WIDGET (container)); + + break; + } + } +} + +static void +gs_hiding_box_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + GsHidingBox *box = GS_HIDING_BOX (container); + GtkWidget *child; + GList *children; + + children = box->children; + while (children) { + child = children->data; + children = children->next; + (* callback) (child, callback_data); + } +} + +static void +gs_hiding_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + GsHidingBox *box = GS_HIDING_BOX (widget); + gint nvis_children = 0; + + GtkTextDirection direction; + GtkAllocation child_allocation; + GtkRequestedSize *sizes; + + gint size; + gint extra = 0; + gint n_extra_widgets = 0; /* Number of widgets that receive 1 extra px */ + gint x = 0, i; + GList *child; + GtkWidget *child_widget; + gint spacing = box->spacing; + gint children_size; + GtkAllocation clip, child_clip; + + gtk_widget_set_allocation (widget, allocation); + + for (child = box->children; child != NULL; child = child->next) { + if (gtk_widget_get_visible (child->data)) + ++nvis_children; + } + + /* If there is no visible child, simply return. */ + if (nvis_children == 0) + return; + + direction = gtk_widget_get_direction (widget); + sizes = g_newa (GtkRequestedSize, nvis_children); + + size = allocation->width; + children_size = -spacing; + /* Retrieve desired size for visible children. */ + for (i = 0, child = box->children; child != NULL; child = child->next) { + + child_widget = GTK_WIDGET (child->data); + if (!gtk_widget_get_visible (child_widget)) + continue; + + gtk_widget_get_preferred_width_for_height (child_widget, + allocation->height, + &sizes[i].minimum_size, + &sizes[i].natural_size); + + /* Assert the api is working properly */ + if (sizes[i].minimum_size < 0) + g_error ("GsHidingBox child %s minimum width: %d < 0 for height %d", + gtk_widget_get_name (child_widget), + sizes[i].minimum_size, allocation->height); + + if (sizes[i].natural_size < sizes[i].minimum_size) + g_error ("GsHidingBox child %s natural width: %d < minimum %d for height %d", + gtk_widget_get_name (child_widget), + sizes[i].natural_size, sizes[i].minimum_size, + allocation->height); + + children_size += sizes[i].minimum_size + spacing; + if (i > 0 && children_size > allocation->width) + break; + + size -= sizes[i].minimum_size; + sizes[i].data = child_widget; + + i++; + } + nvis_children = i; + + /* Bring children up to size first */ + size = gtk_distribute_natural_allocation (MAX (0, size), (guint) nvis_children, sizes); + /* Only now we can subtract the spacings */ + size -= (nvis_children - 1) * spacing; + + if (nvis_children > 1) { + extra = size / nvis_children; + n_extra_widgets = size % nvis_children; + } + + x = allocation->x; + for (i = 0, child = box->children; child != NULL; child = child->next) { + + child_widget = GTK_WIDGET (child->data); + if (!gtk_widget_get_visible (child_widget)) + continue; + + /* Hide the overflowing children even if they have visible=TRUE */ + if (i >= nvis_children) { + while (child) { + gtk_widget_set_child_visible (child->data, FALSE); + child = child->next; + } + break; + } + + child_allocation.x = x; + child_allocation.y = allocation->y; + child_allocation.width = sizes[i].minimum_size + extra; + child_allocation.height = allocation->height; + if (n_extra_widgets) { + ++child_allocation.width; + --n_extra_widgets; + } + if (direction == GTK_TEXT_DIR_RTL) { + child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width; + } + + /* Let this child be visible */ + gtk_widget_set_child_visible (child_widget, TRUE); + gtk_widget_size_allocate (child_widget, &child_allocation); + x += child_allocation.width + spacing; + ++i; + } + + /* + * The code below is inspired by _gtk_widget_set_simple_clip. + * Note: Here we ignore the "box-shadow" CSS property of the + * hiding box because we don't use it. + */ + clip = *allocation; + if (gtk_widget_get_has_window (widget)) { + clip.x = clip.y = 0; + } + + for (child = box->children; child != NULL; child = child->next) { + child_widget = GTK_WIDGET (child->data); + if (gtk_widget_get_visible (child_widget) && + gtk_widget_get_child_visible (child_widget)) { + gtk_widget_get_clip (child_widget, &child_clip); + gdk_rectangle_union (&child_clip, &clip, &clip); + } + } + + if (gtk_widget_get_has_window (widget)) { + clip.x += allocation->x; + clip.y += allocation->y; + } + gtk_widget_set_clip (widget, &clip); +} + +static void +gs_hiding_box_get_preferred_width (GtkWidget *widget, gint *min, gint *nat) +{ + GsHidingBox *box = GS_HIDING_BOX (widget); + gint cm, cn; + gint m, n; + GList *child; + gint nvis_children; + gboolean have_min = FALSE; + + m = n = nvis_children = 0; + for (child = box->children; child != NULL; child = child->next) { + if (!gtk_widget_is_visible (child->data)) + continue; + + ++nvis_children; + gtk_widget_get_preferred_width (child->data, &cm, &cn); + /* Minimum is a minimum of the first visible child */ + if (!have_min) { + m = cm; + have_min = TRUE; + } + /* Natural is a sum of all visible children */ + n += cn; + } + + /* Natural must also include the spacing */ + if (box->spacing && nvis_children > 1) + n += box->spacing * (nvis_children - 1); + + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_hiding_box_get_preferred_height (GtkWidget *widget, gint *min, gint *nat) +{ + gint m, n; + gint cm, cn; + GList *child; + + GsHidingBox *box = GS_HIDING_BOX (widget); + m = n = 0; + for (child = box->children; child != NULL; child = child->next) { + if (!gtk_widget_is_visible (child->data)) + continue; + + gtk_widget_get_preferred_height (child->data, &cm, &cn); + m = MAX (m, cm); + n = MAX (n, cn); + } + + if (min) + *min = m; + if (nat) + *nat = n; +} + +static void +gs_hiding_box_init (GsHidingBox *box) +{ + gtk_widget_set_has_window (GTK_WIDGET (box), FALSE); + gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), FALSE); + + box->spacing = 0; +} + +static void +gs_hiding_box_class_init (GsHidingBoxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); + + object_class->set_property = gs_hiding_box_set_property; + object_class->get_property = gs_hiding_box_get_property; + + widget_class->size_allocate = gs_hiding_box_size_allocate; + widget_class->get_preferred_width = gs_hiding_box_get_preferred_width; + widget_class->get_preferred_height = gs_hiding_box_get_preferred_height; + + container_class->add = gs_hiding_box_add; + container_class->remove = gs_hiding_box_remove; + container_class->forall = gs_hiding_box_forall; + + g_object_class_install_property (object_class, + PROP_SPACING, + g_param_spec_int ("spacing", + "Spacing", + "The amount of space between children", + 0, G_MAXINT, 0, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); +} + +/** + * gs_hiding_box_new: + * + * Creates a new #GsHidingBox. + * + * Returns: a new #GsHidingBox. + **/ +GtkWidget * +gs_hiding_box_new (void) +{ + return g_object_new (GS_TYPE_HIDING_BOX, NULL); +} + +/** + * gs_hiding_box_set_spacing: + * @box: a #GsHidingBox + * @spacing: the number of pixels to put between children + * + * Sets the #GsHidingBox:spacing property of @box, which is the + * number of pixels to place between children of @box. + */ +void +gs_hiding_box_set_spacing (GsHidingBox *box, gint spacing) +{ + g_return_if_fail (GS_IS_HIDING_BOX (box)); + + if (box->spacing != spacing) { + box->spacing = spacing; + + g_object_notify (G_OBJECT (box), "spacing"); + + gtk_widget_queue_resize (GTK_WIDGET (box)); + } +} + +/** + * gs_hiding_box_get_spacing: + * @box: a #GsHidingBox + * + * Gets the value set by gs_hiding_box_set_spacing(). + * + * Returns: spacing between children + **/ +gint +gs_hiding_box_get_spacing (GsHidingBox *box) +{ + g_return_val_if_fail (GS_IS_HIDING_BOX (box), 0); + + return box->spacing; +} diff --git a/src/gs-hiding-box.h b/src/gs-hiding-box.h new file mode 100644 index 0000000..f29e174 --- /dev/null +++ b/src/gs-hiding-box.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_HIDING_BOX (gs_hiding_box_get_type ()) + +G_DECLARE_FINAL_TYPE (GsHidingBox, gs_hiding_box, GS, HIDING_BOX, GtkContainer) + +GtkWidget *gs_hiding_box_new (void); +void gs_hiding_box_set_spacing (GsHidingBox *box, + gint spacing); +gint gs_hiding_box_get_spacing (GsHidingBox *box); + +G_END_DECLS diff --git a/src/gs-history-dialog.c b/src/gs-history-dialog.c new file mode 100644 index 0000000..7863e61 --- /dev/null +++ b/src/gs-history-dialog.c @@ -0,0 +1,239 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-history-dialog.h" +#include "gs-common.h" + +struct _GsHistoryDialog +{ + GtkDialog parent_instance; + + GtkSizeGroup *sizegroup_state; + GtkSizeGroup *sizegroup_timestamp; + GtkSizeGroup *sizegroup_version; + GtkWidget *list_box; + GtkWidget *scrolledwindow; +}; + +G_DEFINE_TYPE (GsHistoryDialog, gs_history_dialog, GTK_TYPE_DIALOG) + +static gint +history_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + guint64 timestamp_a = gs_app_get_install_date (app1); + guint64 timestamp_b = gs_app_get_install_date (app2); + if (timestamp_a < timestamp_b) + return 1; + if (timestamp_a > timestamp_b) + return -1; + return 0; +} + +void +gs_history_dialog_set_app (GsHistoryDialog *dialog, GsApp *app) +{ + const gchar *tmp; + GsAppList *history; + GtkBox *box; + GtkWidget *row; + GtkWidget *widget; + guint64 timestamp; + guint i; + + /* add each history package to the dialog */ + gs_container_remove_all (GTK_CONTAINER (dialog->list_box)); + history = gs_app_get_history (app); + gs_app_list_sort (history, history_sort_cb, NULL); + for (i = 0; i < gs_app_list_length (history); i++) { + g_autoptr(GDateTime) datetime = NULL; + g_autofree gchar *date_str = NULL; + app = gs_app_list_index (history, i); + box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0)); + + /* add the action */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_REMOVING: + /* TRANSLATORS: this is the status in the history UI, + * where we are showing the application was removed */ + tmp = C_("app status", "Removed"); + break; + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_INSTALLING: + /* TRANSLATORS: this is the status in the history UI, + * where we are showing the application was installed */ + tmp = C_("app status", "Installed"); + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + /* TRANSLATORS: this is the status in the history UI, + * where we are showing the application was updated */ + tmp = C_("app status", "Updated"); + break; + default: + /* TRANSLATORS: this is the status in the history UI, + * where we are showing that something happened to the + * application but we don't know what */ + tmp = C_("app status", "Unknown"); + break; + } + widget = gtk_label_new (tmp); + g_object_set (widget, + "margin-start", 20, + "margin-end", 20, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + gtk_size_group_add_widget (dialog->sizegroup_state, widget); + gtk_container_add (GTK_CONTAINER (box), widget); + + /* add the timestamp */ + timestamp = gs_app_get_install_date (app); + datetime = g_date_time_new_from_unix_utc ((gint) timestamp); + if (timestamp == GS_APP_INSTALL_DATE_UNKNOWN) { + date_str = g_strdup (""); + } else { + /* TRANSLATORS: This is the date string with: day number, month name, year. + i.e. "25 May 2012" */ + date_str = g_date_time_format (datetime, _("%e %B %Y")); + } + widget = gtk_label_new (date_str); + g_object_set (widget, + "margin-start", 20, + "margin-end", 20, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + gtk_size_group_add_widget (dialog->sizegroup_timestamp, widget); + gtk_container_add (GTK_CONTAINER (box), widget); + + /* add the version */ + widget = gtk_label_new (gs_app_get_version (app)); + g_object_set (widget, + "margin-start", 20, + "margin-end", 20, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 1.0, + "ellipsize", PANGO_ELLIPSIZE_END, + "width-chars", 10, + "hexpand", TRUE, + NULL); + gtk_size_group_add_widget (dialog->sizegroup_version, widget); + gtk_container_add (GTK_CONTAINER (box), widget); + + gtk_widget_show_all (GTK_WIDGET (box)); + gtk_list_box_insert (GTK_LIST_BOX (dialog->list_box), GTK_WIDGET (box), -1); + + row = gtk_widget_get_parent (GTK_WIDGET (box)); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE); + } +} + +static void +update_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header; + + /* first entry */ + header = gtk_list_box_row_get_header (row); + if (before == NULL) { + gtk_list_box_row_set_header (row, NULL); + return; + } + + /* already set */ + if (header != NULL) + return; + + /* set new */ + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static void +scrollbar_mapped_cb (GtkWidget *sb, GtkScrolledWindow *swin) +{ + GtkWidget *frame; + + frame = gtk_bin_get_child (GTK_BIN (gtk_bin_get_child (GTK_BIN (swin)))); + if (gtk_widget_get_mapped (GTK_WIDGET (sb))) { + gtk_scrolled_window_set_shadow_type (swin, GTK_SHADOW_IN); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_NONE); + } else { + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN); + gtk_scrolled_window_set_shadow_type (swin, GTK_SHADOW_NONE); + } +} + +static void +gs_history_dialog_dispose (GObject *object) +{ + GsHistoryDialog *dialog = GS_HISTORY_DIALOG (object); + + g_clear_object (&dialog->sizegroup_state); + g_clear_object (&dialog->sizegroup_timestamp); + g_clear_object (&dialog->sizegroup_version); + + G_OBJECT_CLASS (gs_history_dialog_parent_class)->dispose (object); +} + +static void +gs_history_dialog_init (GsHistoryDialog *dialog) +{ + GtkWidget *scrollbar; + + gtk_widget_init_template (GTK_WIDGET (dialog)); + + dialog->sizegroup_state = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + dialog->sizegroup_timestamp = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + dialog->sizegroup_version = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + gtk_list_box_set_header_func (GTK_LIST_BOX (dialog->list_box), + update_header_func, + dialog, + NULL); + + scrollbar = gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (dialog->scrolledwindow)); + g_signal_connect (scrollbar, "map", G_CALLBACK (scrollbar_mapped_cb), dialog->scrolledwindow); + g_signal_connect (scrollbar, "unmap", G_CALLBACK (scrollbar_mapped_cb), dialog->scrolledwindow); +} + +static void +gs_history_dialog_class_init (GsHistoryDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_history_dialog_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-history-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsHistoryDialog, list_box); + gtk_widget_class_bind_template_child (widget_class, GsHistoryDialog, scrolledwindow); +} + +GtkWidget * +gs_history_dialog_new (void) +{ + return GTK_WIDGET (g_object_new (GS_TYPE_HISTORY_DIALOG, + "use-header-bar", TRUE, + NULL)); +} diff --git a/src/gs-history-dialog.h b/src/gs-history-dialog.h new file mode 100644 index 0000000..ebbec9e --- /dev/null +++ b/src/gs-history-dialog.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_HISTORY_DIALOG (gs_history_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsHistoryDialog, gs_history_dialog, GS, HISTORY_DIALOG, GtkDialog) + +GtkWidget *gs_history_dialog_new (void); +void gs_history_dialog_set_app (GsHistoryDialog *dialog, + GsApp *app); + +G_END_DECLS diff --git a/src/gs-history-dialog.ui b/src/gs-history-dialog.ui new file mode 100644 index 0000000..942e2a5 --- /dev/null +++ b/src/gs-history-dialog.ui @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsHistoryDialog" parent="GtkDialog"> + <property name="title" translatable="yes">History</property> + <property name="modal">True</property> + <property name="default_width">600</property> + <property name="default_height">300</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="use_header_bar">1</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox"> + <property name="border_width">0</property> + <property name="orientation">vertical</property> + <property name="spacing">9</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">none</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="vexpand">True</property> + <child> + <object class="GtkFrame" id="frame"> + <property name="visible">True</property> + <property name="shadow_type">none</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <style> + <class name="view"/> + </style> + <child> + <object class="GtkListBox" id="list_box"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-info-bar.c b/src/gs-info-bar.c new file mode 100644 index 0000000..caac2ec --- /dev/null +++ b/src/gs-info-bar.c @@ -0,0 +1,199 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-info-bar.h" + +struct _GsInfoBar +{ + GtkInfoBar parent_instance; + + GtkWidget *label_title; + GtkWidget *label_body; + GtkWidget *label_warning; +}; + +G_DEFINE_TYPE (GsInfoBar, gs_info_bar, GTK_TYPE_INFO_BAR) + +enum { + PROP_0, + PROP_TITLE, + PROP_BODY, + PROP_WARNING +}; + +static void +gs_info_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsInfoBar *infobar = GS_INFO_BAR (object); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, gs_info_bar_get_title (infobar)); + break; + case PROP_BODY: + g_value_set_string (value, gs_info_bar_get_body (infobar)); + break; + case PROP_WARNING: + g_value_set_string (value, gs_info_bar_get_warning (infobar)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_info_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsInfoBar *infobar = GS_INFO_BAR (object); + + switch (prop_id) { + case PROP_TITLE: + gs_info_bar_set_title (infobar, g_value_get_string (value)); + break; + case PROP_BODY: + gs_info_bar_set_body (infobar, g_value_get_string (value)); + break; + case PROP_WARNING: + gs_info_bar_set_warning (infobar, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_info_bar_init (GsInfoBar *infobar) +{ + gtk_widget_set_has_window (GTK_WIDGET (infobar), FALSE); + gtk_widget_init_template (GTK_WIDGET (infobar)); +} + +static void +gs_info_bar_class_init (GsInfoBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_info_bar_get_property; + object_class->set_property = gs_info_bar_set_property; + + g_object_class_install_property (object_class, PROP_TITLE, + g_param_spec_string ("title", + "Title Text", + "The title (header) text of the info bar", + "", + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_BODY, + g_param_spec_string ("body", + "Body Text", + "The body (main) text of the info bar", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_WARNING, + g_param_spec_string ("warning", + "Warning Text", + "The warning text of the info bar, below the body text", + NULL, + G_PARAM_READWRITE)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-info-bar.ui"); + + gtk_widget_class_bind_template_child (widget_class, + GsInfoBar, label_title); + gtk_widget_class_bind_template_child (widget_class, + GsInfoBar, label_body); + gtk_widget_class_bind_template_child (widget_class, + GsInfoBar, label_warning); +} + +GtkWidget * +gs_info_bar_new (void) +{ + GsInfoBar *infobar; + + infobar = g_object_new (GS_TYPE_INFO_BAR, NULL); + + return GTK_WIDGET (infobar); +} + +static const gchar * +retrieve_from_label (GtkWidget *label) +{ + if (!gtk_widget_get_visible (label)) + return NULL; + else + return gtk_label_get_label (GTK_LABEL (label)); +} + +static void +update_label (GtkWidget *label, const gchar *text) +{ + gtk_label_set_label (GTK_LABEL (label), text); + gtk_widget_set_visible (label, + text != NULL && *text != '\0'); +} + +const gchar * +gs_info_bar_get_title (GsInfoBar *bar) +{ + g_return_val_if_fail (GS_IS_INFO_BAR (bar), NULL); + + return retrieve_from_label (bar->label_title); +} + +void +gs_info_bar_set_title (GsInfoBar *bar, const gchar *text) +{ + g_return_if_fail (GS_IS_INFO_BAR (bar)); + + update_label (bar->label_title, text); +} + +const gchar * +gs_info_bar_get_body (GsInfoBar *bar) +{ + g_return_val_if_fail (GS_IS_INFO_BAR (bar), NULL); + + return retrieve_from_label (bar->label_body); +} + +void +gs_info_bar_set_body (GsInfoBar *bar, const gchar *text) +{ + g_return_if_fail (GS_IS_INFO_BAR (bar)); + + update_label (bar->label_body, text); +} + +const gchar * +gs_info_bar_get_warning (GsInfoBar *bar) +{ + g_return_val_if_fail (GS_IS_INFO_BAR (bar), NULL); + + return retrieve_from_label (bar->label_warning); +} + +void +gs_info_bar_set_warning (GsInfoBar *bar, const gchar *text) +{ + g_return_if_fail (GS_IS_INFO_BAR (bar)); + + update_label (bar->label_warning, text); +} diff --git a/src/gs-info-bar.h b/src/gs-info-bar.h new file mode 100644 index 0000000..7001867 --- /dev/null +++ b/src/gs-info-bar.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Rafał Lużyński <digitalfreak@lingonborough.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_INFO_BAR (gs_info_bar_get_type ()) + +G_DECLARE_FINAL_TYPE (GsInfoBar, gs_info_bar, GS, INFO_BAR, GtkInfoBar) + +GtkWidget *gs_info_bar_new (void); +const gchar *gs_info_bar_get_title (GsInfoBar *bar); +void gs_info_bar_set_title (GsInfoBar *bar, + const gchar *text); +const gchar *gs_info_bar_get_body (GsInfoBar *bar); +void gs_info_bar_set_body (GsInfoBar *bar, + const gchar *text); +const gchar *gs_info_bar_get_warning (GsInfoBar *bar); +void gs_info_bar_set_warning (GsInfoBar *bar, + const gchar *text); +G_END_DECLS diff --git a/src/gs-info-bar.ui b/src/gs-info-bar.ui new file mode 100644 index 0000000..8977442 --- /dev/null +++ b/src/gs-info-bar.ui @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsInfoBar" parent="GtkInfoBar"> + <property name="app_paintable">True</property> + <property name="message_type">info</property> + <style> + <class name="application-details-infobar"/> + </style> + <child internal-child="content_area"> + <object class="GtkBox" id="content_area"> + <property name="spacing">0</property> + <property name="halign">fill</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label_title"> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max_width_chars">30</property> + <property name="visible">False</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label_body"> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max_width_chars">30</property> + <property name="visible">False</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_warning"> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max_width_chars">30</property> + <property name="visible">False</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-installed-page.c b/src/gs-installed-page.c new file mode 100644 index 0000000..3688be4 --- /dev/null +++ b/src/gs-installed-page.c @@ -0,0 +1,633 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-shell.h" +#include "gs-installed-page.h" +#include "gs-common.h" +#include "gs-app-row.h" +#include "gs-utils.h" + +struct _GsInstalledPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + gboolean cache_valid; + gboolean waiting; + GsShell *shell; + GSettings *settings; + + GtkWidget *list_box_install; + GtkWidget *scrolledwindow_install; + GtkWidget *spinner_install; + GtkWidget *stack_install; +}; + +G_DEFINE_TYPE (GsInstalledPage, gs_installed_page, GS_TYPE_PAGE) + +static void gs_installed_page_pending_apps_changed_cb (GsPluginLoader *plugin_loader, + GsInstalledPage *self); + +static void +gs_installed_page_invalidate (GsInstalledPage *self) +{ + self->cache_valid = FALSE; +} + +static void +gs_installed_page_app_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsInstalledPage *self) +{ + GsApp *app; + app = gs_app_row_get_app (GS_APP_ROW (row)); + gs_shell_show_app (self->shell, app); +} + +static void +row_unrevealed (GObject *row, GParamSpec *pspec, gpointer data) +{ + GtkWidget *list; + + list = gtk_widget_get_parent (GTK_WIDGET (row)); + if (list == NULL) + return; + gtk_container_remove (GTK_CONTAINER (list), GTK_WIDGET (row)); +} + +static void +gs_installed_page_unreveal_row (GsAppRow *app_row) +{ + g_signal_connect (app_row, "unrevealed", + G_CALLBACK (row_unrevealed), NULL); + gs_app_row_unreveal (app_row); +} + +static void +gs_installed_page_app_removed (GsPage *page, GsApp *app) +{ + GsInstalledPage *self = GS_INSTALLED_PAGE (page); + g_autoptr(GList) children = NULL; + + children = gtk_container_get_children (GTK_CONTAINER (self->list_box_install)); + for (GList *l = children; l; l = l->next) { + GsAppRow *app_row = GS_APP_ROW (l->data); + if (gs_app_row_get_app (app_row) == app) { + gs_installed_page_unreveal_row (app_row); + } + } +} + +static void +gs_installed_page_app_remove_cb (GsAppRow *app_row, + GsInstalledPage *self) +{ + GsApp *app; + + app = gs_app_row_get_app (app_row); + gs_page_remove_app (GS_PAGE (self), app, self->cancellable); +} + +static gboolean +gs_installed_page_invalidate_sort_idle (gpointer user_data) +{ + GsAppRow *app_row = user_data; + GsApp *app = gs_app_row_get_app (app_row); + AsAppState state = gs_app_get_state (app); + + gtk_list_box_row_changed (GTK_LIST_BOX_ROW (app_row)); + + /* if the app has been uninstalled (which can happen from another view) + * we should removed it from the installed view */ + if (state == AS_APP_STATE_AVAILABLE || state == AS_APP_STATE_UNKNOWN) + gs_installed_page_unreveal_row (app_row); + + g_object_unref (app_row); + return G_SOURCE_REMOVE; +} + +static void +gs_installed_page_notify_state_changed_cb (GsApp *app, + GParamSpec *pspec, + GsAppRow *app_row) +{ + g_idle_add (gs_installed_page_invalidate_sort_idle, g_object_ref (app_row)); +} + +static gboolean +should_show_installed_size (GsInstalledPage *self) +{ + return g_settings_get_boolean (self->settings, + "installed-page-show-size"); +} + +static gboolean +gs_installed_page_is_actual_app (GsApp *app) +{ + if (gs_app_get_description (app) != NULL) + return TRUE; + /* special snowflake */ + if (g_strcmp0 (gs_app_get_id (app), "google-chrome.desktop") == 0) + return TRUE; + g_debug ("%s is not an actual app", gs_app_get_unique_id (app)); + return FALSE; +} + +static void +gs_installed_page_add_app (GsInstalledPage *self, GsAppList *list, GsApp *app) +{ + GtkWidget *app_row; + + app_row = g_object_new (GS_TYPE_APP_ROW, + "app", app, + "show-buttons", TRUE, + "show-source", gs_utils_list_has_app_fuzzy (list, app), + "show-installed-size", !gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY) && should_show_installed_size (self), + NULL); + + g_signal_connect (app_row, "button-clicked", + G_CALLBACK (gs_installed_page_app_remove_cb), self); + g_signal_connect_object (app, "notify::state", + G_CALLBACK (gs_installed_page_notify_state_changed_cb), + app_row, 0); + gtk_container_add (GTK_CONTAINER (self->list_box_install), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button); + + /* only show if is an actual application */ + gtk_widget_set_visible (app_row, gs_installed_page_is_actual_app (app)); +} + +static void +gs_installed_page_get_installed_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + guint i; + GsApp *app; + GsInstalledPage *self = GS_INSTALLED_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + gs_stop_spinner (GTK_SPINNER (self->spinner_install)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "view"); + + self->waiting = FALSE; + self->cache_valid = TRUE; + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get installed apps: %s", error->message); + goto out; + } + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_installed_page_add_app (self, list, app); + } +out: + gs_installed_page_pending_apps_changed_cb (plugin_loader, self); +} + +static void +gs_installed_page_load (GsInstalledPage *self) +{ + GsPluginRefineFlags flags; + g_autoptr(GsPluginJob) plugin_job = NULL; + + if (self->waiting) + return; + self->waiting = TRUE; + + /* remove old entries */ + gs_container_remove_all (GTK_CONTAINER (self->list_box_install)); + + flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING; + + if (should_show_installed_size (self)) + flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE; + + /* get installed apps */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_INSTALLED, + "refine-flags", flags, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, + plugin_job, + self->cancellable, + gs_installed_page_get_installed_cb, + self); + gs_start_spinner (GTK_SPINNER (self->spinner_install)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "spinner"); +} + +static void +gs_installed_page_reload (GsPage *page) +{ + GsInstalledPage *self = GS_INSTALLED_PAGE (page); + gs_installed_page_invalidate (self); + gs_installed_page_load (self); +} + +static void +gs_installed_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsInstalledPage *self = GS_INSTALLED_PAGE (page); + GtkWidget *widget; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_INSTALLED) { + g_warning ("Called switch_to(installed) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "buttonbox_main")); + gtk_widget_show (widget); + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "menu_button")); + gtk_widget_show (widget); + + if (scroll_up) { + GtkAdjustment *adj; + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_install)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } + if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_INSTALLED) { + gs_grab_focus_when_mapped (self->scrolledwindow_install); + } + + /* no need to refresh */ + if (self->cache_valid) + return; + + gs_installed_page_load (self); +} + +/** + * gs_installed_page_get_app_sort_key: + * + * Get a sort key to achive this: + * + * 1. state:installing applications + * 2. state: applications queued for installing + * 3. state:removing applications + * 4. kind:normal applications + * 5. kind:system applications + * + * Within each of these groups, they are sorted by the install date and then + * by name. + **/ +static gchar * +gs_installed_page_get_app_sort_key (GsApp *app) +{ + GString *key; + g_autofree gchar *sort_name = NULL; + + key = g_string_sized_new (64); + + /* sort installed, removing, other */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLING: + g_string_append (key, "1:"); + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + g_string_append (key, "2:"); + break; + case AS_APP_STATE_REMOVING: + g_string_append (key, "3:"); + break; + default: + g_string_append (key, "4:"); + break; + } + + /* sort apps by kind */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_OS_UPDATE: + g_string_append (key, "1:"); + break; + case AS_APP_KIND_DESKTOP: + g_string_append (key, "2:"); + break; + case AS_APP_KIND_WEB_APP: + g_string_append (key, "3:"); + break; + case AS_APP_KIND_RUNTIME: + g_string_append (key, "4:"); + break; + case AS_APP_KIND_ADDON: + g_string_append (key, "5:"); + break; + case AS_APP_KIND_CODEC: + g_string_append (key, "6:"); + break; + case AS_APP_KIND_FONT: + g_string_append (key, "6:"); + break; + case AS_APP_KIND_INPUT_METHOD: + g_string_append (key, "7:"); + break; + case AS_APP_KIND_SHELL_EXTENSION: + g_string_append (key, "8:"); + break; + default: + g_string_append (key, "9:"); + break; + } + + /* sort normal, compulsory */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY)) + g_string_append (key, "1:"); + else + g_string_append (key, "2:"); + + /* finally, sort by short name */ + if (gs_app_get_name (app) != NULL) { + sort_name = gs_utils_sort_key (gs_app_get_name (app)); + g_string_append (key, sort_name); + } + + return g_string_free (key, FALSE); +} + +static gint +gs_installed_page_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GsApp *a1, *a2; + g_autofree gchar *key1 = NULL; + g_autofree gchar *key2 = NULL; + + /* check valid */ + if (!GTK_IS_BIN(a) || !GTK_IS_BIN(b)) { + g_warning ("GtkListBoxRow not valid"); + return 0; + } + + a1 = gs_app_row_get_app (GS_APP_ROW (a)); + a2 = gs_app_row_get_app (GS_APP_ROW (b)); + key1 = gs_installed_page_get_app_sort_key (a1); + key2 = gs_installed_page_get_app_sort_key (a2); + + /* compare the keys according to the algorithm above */ + return g_strcmp0 (key1, key2); +} + +typedef enum { + GS_UPDATE_LIST_SECTION_REMOVABLE_APPS, + GS_UPDATE_LIST_SECTION_SYSTEM_APPS, + GS_UPDATE_LIST_SECTION_ADDONS, + GS_UPDATE_LIST_SECTION_LAST +} GsInstalledPageSection; + +static GsInstalledPageSection +gs_installed_page_get_app_section (GsApp *app) +{ + if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP || + gs_app_get_kind (app) == AS_APP_KIND_WEB_APP) { + if (gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY)) + return GS_UPDATE_LIST_SECTION_SYSTEM_APPS; + return GS_UPDATE_LIST_SECTION_REMOVABLE_APPS; + } + return GS_UPDATE_LIST_SECTION_ADDONS; +} + +static GtkWidget * +gs_installed_page_get_section_header (GsInstalledPageSection section) +{ + GtkWidget *header = NULL; + + if (section == GS_UPDATE_LIST_SECTION_SYSTEM_APPS) { + /* TRANSLATORS: This is the header dividing the normal + * applications and the system ones */ + header = gtk_label_new (_("System Applications")); + } else if (section == GS_UPDATE_LIST_SECTION_ADDONS) { + /* TRANSLATORS: This is the header dividing the normal + * applications and the addons */ + header = gtk_label_new (_("Add-ons")); + } + + /* fix header style */ + if (header != NULL) { + GtkStyleContext *context = gtk_widget_get_style_context (header); + gtk_label_set_xalign (GTK_LABEL (header), 0.0); + gtk_style_context_add_class (context, "app-listbox-header"); + gtk_style_context_add_class (context, "app-listbox-header-title"); + } + return header; +} + +static void +gs_installed_page_list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GsApp *app = gs_app_row_get_app (GS_APP_ROW (row)); + GsInstalledPageSection before_section = GS_UPDATE_LIST_SECTION_LAST; + GsInstalledPageSection section; + GtkWidget *header; + + /* first entry */ + gtk_list_box_row_set_header (row, NULL); + if (before != NULL) { + GsApp *before_app = gs_app_row_get_app (GS_APP_ROW (before)); + before_section = gs_installed_page_get_app_section (before_app); + } + + /* section changed or forced to have headers */ + section = gs_installed_page_get_app_section (app); + if (before_section != section) { + header = gs_installed_page_get_section_header (section); + if (header == NULL) + return; + } else { + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + } + gtk_list_box_row_set_header (row, header); +} + +static gboolean +gs_installed_page_has_app (GsInstalledPage *self, + GsApp *app) +{ + gboolean ret = FALSE; + g_autoptr(GList) children = NULL; + + children = gtk_container_get_children (GTK_CONTAINER (self->list_box_install)); + for (GList *l = children; l; l = l->next) { + GsAppRow *app_row = GS_APP_ROW (l->data); + if (gs_app_row_get_app (app_row) == app) { + ret = TRUE; + break; + } + } + return ret; +} + +static void +gs_installed_page_pending_apps_changed_cb (GsPluginLoader *plugin_loader, + GsInstalledPage *self) +{ + GsApp *app; + GtkWidget *widget; + guint i; + guint cnt = 0; + g_autoptr(GsAppList) pending = NULL; + + /* add new apps to the list */ + pending = gs_plugin_loader_get_pending (plugin_loader); + for (i = 0; i < gs_app_list_length (pending); i++) { + app = gs_app_list_index (pending, i); + + /* never show OS upgrades, we handle the scheduling and + * cancellation in GsUpgradeBanner */ + if (gs_app_get_kind (app) == AS_APP_KIND_OS_UPGRADE) + continue; + + /* do not to add pending apps more than once. */ + if (gs_installed_page_has_app (self, app) == FALSE) + gs_installed_page_add_app (self, pending, app); + + /* incremement the label */ + cnt++; + } + + /* show a label with the number of on-going operations */ + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, + "button_installed_counter")); + if (cnt == 0) { + gtk_widget_hide (widget); + } else { + g_autofree gchar *label = NULL; + label = g_strdup_printf ("%u", cnt); + gtk_label_set_label (GTK_LABEL (widget), label); + gtk_widget_show (widget); + } +} + +static gboolean +gs_installed_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsInstalledPage *self = GS_INSTALLED_PAGE (page); + + g_return_val_if_fail (GS_IS_INSTALLED_PAGE (self), TRUE); + + self->shell = shell; + self->plugin_loader = g_object_ref (plugin_loader); + g_signal_connect (self->plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_installed_page_pending_apps_changed_cb), + self); + + self->builder = g_object_ref (builder); + self->cancellable = g_object_ref (cancellable); + + /* setup installed */ + g_signal_connect (self->list_box_install, "row-activated", + G_CALLBACK (gs_installed_page_app_row_activated_cb), self); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_install), + gs_installed_page_list_header_func, + self, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install), + gs_installed_page_sort_func, + self, NULL); + return TRUE; +} + +static void +gs_installed_page_dispose (GObject *object) +{ + GsInstalledPage *self = GS_INSTALLED_PAGE (object); + + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + + g_clear_object (&self->builder); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (gs_installed_page_parent_class)->dispose (object); +} + +static void +gs_installed_page_class_init (GsInstalledPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_installed_page_dispose; + page_class->app_removed = gs_installed_page_app_removed; + page_class->switch_to = gs_installed_page_switch_to; + page_class->reload = gs_installed_page_reload; + page_class->setup = gs_installed_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-installed-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install); + gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, scrolledwindow_install); + gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, spinner_install); + gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, stack_install); +} + +static void +gs_installed_page_init (GsInstalledPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + self->settings = g_settings_new ("org.gnome.software"); +} + +GsInstalledPage * +gs_installed_page_new (void) +{ + GsInstalledPage *self; + self = g_object_new (GS_TYPE_INSTALLED_PAGE, NULL); + return GS_INSTALLED_PAGE (self); +} diff --git a/src/gs-installed-page.h b/src/gs-installed-page.h new file mode 100644 index 0000000..367250d --- /dev/null +++ b/src/gs-installed-page.h @@ -0,0 +1,22 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_INSTALLED_PAGE (gs_installed_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsInstalledPage, gs_installed_page, GS, INSTALLED_PAGE, GsPage) + +GsInstalledPage *gs_installed_page_new (void); + +G_END_DECLS diff --git a/src/gs-installed-page.ui b/src/gs-installed-page.ui new file mode 100644 index 0000000..ec265e0 --- /dev/null +++ b/src/gs-installed-page.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsInstalledPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="installed-accessible"> + <property name="accessible-name" translatable="yes">Installed page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack_install"> + <property name="visible">True</property> + <child> + <object class="GtkSpinner" id="spinner_install"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_install"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_install"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="vexpand">True</property> + <property name="shadow_type">none</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <child> + <object class="GtkListBox" id="list_box_install"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">view</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-language.c b/src/gs-language.c new file mode 100644 index 0000000..d696b37 --- /dev/null +++ b/src/gs-language.c @@ -0,0 +1,163 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> + +#include "gs-language.h" + +struct _GsLanguage +{ + GObject parent_instance; + + GHashTable *hash; +}; + +G_DEFINE_TYPE (GsLanguage, gs_language, G_TYPE_OBJECT) + +static void +gs_language_parser_start_element (GMarkupParseContext *context, const gchar *element_name, + const gchar **attribute_names, const gchar **attribute_values, + gpointer user_data, GError **error) +{ + guint i, len; + const gchar *code1 = NULL; + const gchar *code2b = NULL; + const gchar *name = NULL; + GsLanguage *language = user_data; + + if (strcmp (element_name, "iso_639_entry") != 0) + return; + + /* find data */ + len = g_strv_length ((gchar**)attribute_names); + for (i=0; i<len; i++) { + if (strcmp (attribute_names[i], "iso_639_1_code") == 0) + code1 = attribute_values[i]; + if (strcmp (attribute_names[i], "iso_639_2B_code") == 0) + code2b = attribute_values[i]; + if (strcmp (attribute_names[i], "name") == 0) + name = attribute_values[i]; + } + + /* not valid entry */ + if (name == NULL) + return; + + /* add both to hash */ + if (code1 != NULL) + g_hash_table_insert (language->hash, g_strdup (code1), g_strdup (name)); + if (code2b != NULL) + g_hash_table_insert (language->hash, g_strdup (code2b), g_strdup (name)); +} + +/* trivial parser */ +static const GMarkupParser gs_language_markup_parser = +{ + gs_language_parser_start_element, + NULL, /* end_element */ + NULL, /* characters */ + NULL, /* passthrough */ + NULL /* error */ +}; + +/** + * gs_language_populate: + * + * <iso_639_entry iso_639_2B_code="hun" iso_639_2T_code="hun" iso_639_1_code="hu" name="Hungarian" /> + **/ +gboolean +gs_language_populate (GsLanguage *language, GError **error) +{ + gsize size; + g_autofree gchar *contents = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(GMarkupParseContext) context = NULL; + + /* find filename */ + filename = g_build_filename (DATADIR, "xml", "iso-codes", "iso_639.xml", NULL); + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { + g_free (filename); + filename = g_build_filename ("/usr", "share", "xml", "iso-codes", "iso_639.xml", NULL); + } + /* FreeBSD and OpenBSD ports */ + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { + g_free (filename); + filename = g_build_filename ("/usr", "local", "share", "xml", "iso-codes", "iso_639.xml", NULL); + } + /* NetBSD pkgsrc */ + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { + g_free (filename); + filename = g_build_filename ("/usr", "pkg", "share", "xml", "iso-codes", "iso_639.xml", NULL); + } + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { + g_set_error (error, 1, 0, "cannot find source file : '%s'", filename); + return FALSE; + } + + /* get contents */ + if (!g_file_get_contents (filename, &contents, &size, error)) + return FALSE; + + /* create parser */ + context = g_markup_parse_context_new (&gs_language_markup_parser, G_MARKUP_PREFIX_ERROR_POSITION, language, NULL); + + /* parse data */ + if (!g_markup_parse_context_parse (context, contents, (gssize) size, error)) + return FALSE; + + return TRUE;; +} + +gchar * +gs_language_iso639_to_language (GsLanguage *language, const gchar *iso639) +{ + return g_strdup (g_hash_table_lookup (language->hash, iso639)); +} + +static void +gs_language_finalize (GObject *object) +{ + GsLanguage *language; + + g_return_if_fail (GS_IS_LANGUAGE (object)); + + language = GS_LANGUAGE (object); + + g_hash_table_unref (language->hash); + + G_OBJECT_CLASS (gs_language_parent_class)->finalize (object); +} + +static void +gs_language_init (GsLanguage *language) +{ + language->hash = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_free); +} + +static void +gs_language_class_init (GsLanguageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_language_finalize; +} + +/** + * gs_language_new: + * + * Return value: a new GsLanguage object. + **/ +GsLanguage * +gs_language_new (void) +{ + GsLanguage *language; + language = g_object_new (GS_TYPE_LANGUAGE, NULL); + return GS_LANGUAGE (language); +} diff --git a/src/gs-language.h b/src/gs-language.h new file mode 100644 index 0000000..04688f8 --- /dev/null +++ b/src/gs-language.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_LANGUAGE (gs_language_get_type ()) + +G_DECLARE_FINAL_TYPE (GsLanguage, gs_language, GS, LANGUAGE, GObject) + +GsLanguage *gs_language_new (void); +gboolean gs_language_populate (GsLanguage *language, + GError **error); +gchar *gs_language_iso639_to_language (GsLanguage *language, + const gchar *iso639); + +G_END_DECLS diff --git a/src/gs-loading-page.c b/src/gs-loading-page.c new file mode 100644 index 0000000..67a20fb --- /dev/null +++ b/src/gs-loading-page.c @@ -0,0 +1,234 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-shell.h" +#include "gs-loading-page.h" + +typedef struct { + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GCancellable *cancellable; + GsShell *shell; + + GtkWidget *progressbar; + GtkWidget *label; + guint progress_pulse_id; +} GsLoadingPagePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsLoadingPage, gs_loading_page, GS_TYPE_PAGE) + +enum { + SIGNAL_REFRESHED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +static gboolean +_pulse_cb (gpointer user_data) +{ + GsLoadingPage *self = GS_LOADING_PAGE (user_data); + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + gtk_progress_bar_pulse (GTK_PROGRESS_BAR (priv->progressbar)); + return TRUE; +} + +static void +gs_loading_page_status_changed_cb (GsPluginLoader *plugin_loader, + GsApp *app, + GsPluginStatus status, + GsLoadingPage *self) +{ + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + const gchar *str = NULL; + + /* update label */ + if (status == GS_PLUGIN_STATUS_DOWNLOADING) { + if (app != NULL) + str = gs_app_get_summary_missing (app); + if (str == NULL) { + /* TRANSLATORS: initial start */ + str = _("Software catalog is being downloaded"); + } + } else { + /* TRANSLATORS: initial start */ + str = _("Software catalog is being downloaded"); + } + + /* update label */ + gtk_label_set_label (GTK_LABEL (priv->label), str); + + /* update progresbar */ + if (app != NULL) { + if (priv->progress_pulse_id != 0) { + g_source_remove (priv->progress_pulse_id); + priv->progress_pulse_id = 0; + } + + if (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN) { + priv->progress_pulse_id = g_timeout_add (50, _pulse_cb, self); + } else { + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->progressbar), + (gdouble) gs_app_get_progress (app) / 100.0f); + } + } +} + +static void +gs_loading_page_refresh_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + GsLoadingPage *self = GS_LOADING_PAGE (user_data); + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + g_autoptr(GError) error = NULL; + + /* no longer care */ + g_signal_handlers_disconnect_by_data (plugin_loader, self); + + /* not sure how to handle this */ + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to load metadata: %s", error->message); + } + + /* no more pulsing */ + if (priv->progress_pulse_id != 0) { + g_source_remove (priv->progress_pulse_id); + priv->progress_pulse_id = 0; + } + + /* UI is good to go */ + g_signal_emit (self, signals[SIGNAL_REFRESHED], 0); +} + +static void +gs_loading_page_load (GsLoadingPage *self) +{ + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GSettings) settings = NULL; + guint64 cache_age; + + /* Ensure that at least some metadata of any age is present, and also + * spin up the plugins enough as to prime caches. If this is the first + * run of gnome-software, set the cache age to 24h to ensure that the + * metadata is refreshed if, for example, this is the first boot of a + * computer which has been in storage (after manufacture) for a while. + * Otherwise, set the cache age to the maximum, to only refresh if we’re + * completely missing app data — otherwise, we want to start up as fast + * as possible. */ + settings = g_settings_new ("org.gnome.software"); + if (g_settings_get_boolean (settings, "first-run")) + cache_age = 60 * 60 * 24; /* 24 hours */ + else + cache_age = G_MAXUINT; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", cache_age, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + priv->cancellable, + gs_loading_page_refresh_cb, + self); + g_signal_connect (priv->plugin_loader, "status-changed", + G_CALLBACK (gs_loading_page_status_changed_cb), + self); +} + +static void +gs_loading_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsLoadingPage *self = GS_LOADING_PAGE (page); + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + + if (gs_shell_get_mode (priv->shell) != GS_SHELL_MODE_LOADING) { + g_warning ("Called switch_to(loading) when in mode %s", + gs_shell_get_mode_string (priv->shell)); + return; + } + gs_loading_page_load (self); +} + +static gboolean +gs_loading_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsLoadingPage *self = GS_LOADING_PAGE (page); + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + + g_return_val_if_fail (GS_IS_LOADING_PAGE (self), TRUE); + + priv->shell = shell; + priv->plugin_loader = g_object_ref (plugin_loader); + priv->cancellable = g_object_ref (cancellable); + return TRUE; +} + +static void +gs_loading_page_dispose (GObject *object) +{ + GsLoadingPage *self = GS_LOADING_PAGE (object); + GsLoadingPagePrivate *priv = gs_loading_page_get_instance_private (self); + + if (priv->progress_pulse_id != 0) { + g_source_remove (priv->progress_pulse_id); + priv->progress_pulse_id = 0; + } + + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->cancellable); + + G_OBJECT_CLASS (gs_loading_page_parent_class)->dispose (object); +} + +static void +gs_loading_page_class_init (GsLoadingPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_loading_page_dispose; + page_class->switch_to = gs_loading_page_switch_to; + page_class->setup = gs_loading_page_setup; + + signals [SIGNAL_REFRESHED] = + g_signal_new ("refreshed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsLoadingPageClass, refreshed), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-loading-page.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsLoadingPage, progressbar); + gtk_widget_class_bind_template_child_private (widget_class, GsLoadingPage, label); +} + +static void +gs_loading_page_init (GsLoadingPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GsLoadingPage * +gs_loading_page_new (void) +{ + GsLoadingPage *self; + self = g_object_new (GS_TYPE_LOADING_PAGE, NULL); + return GS_LOADING_PAGE (self); +} diff --git a/src/gs-loading-page.h b/src/gs-loading-page.h new file mode 100644 index 0000000..7add3d9 --- /dev/null +++ b/src/gs-loading-page.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_LOADING_PAGE (gs_loading_page_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsLoadingPage, gs_loading_page, GS, LOADING_PAGE, GsPage) + +struct _GsLoadingPageClass +{ + GsPageClass parent_class; + + void (*refreshed) (GsLoadingPage *self); +}; + +GsLoadingPage *gs_loading_page_new (void); + +G_END_DECLS diff --git a/src/gs-loading-page.ui b/src/gs-loading-page.ui new file mode 100644 index 0000000..7120e5c --- /dev/null +++ b/src/gs-loading-page.ui @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsLoadingPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="loading-accessible"> + <property name="accessible-name" translatable="yes">Loading page</property> + </object> + </child> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">48</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child type="center"> + <object class="GtkBox" id="centerbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="pixel_size">256</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + </object> + </child> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="width_request">480</property> + <property name="halign">center</property> + <property name="fraction">0.0</property> + <property name="margin_top">8</property> + <style> + <class name="upgrade-progressbar"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Starting up…</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-main.c b/src/gs-main.c new file mode 100644 index 0000000..76837ab --- /dev/null +++ b/src/gs-main.c @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> +#include <gtk/gtk.h> +#include <locale.h> +#include <sys/stat.h> + +#include "gs-application.h" +#include "gs-debug.h" + +int +main (int argc, char **argv) +{ + int status = 0; + g_autoptr(GDesktopAppInfo) appinfo = NULL; + g_autoptr(GsApplication) application = NULL; + g_autoptr(GsDebug) debug = gs_debug_new (); + + g_set_prgname ("org.gnome.Software"); + setlocale (LC_ALL, ""); + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + /* Override the umask to 022 to make it possible to share files between + * the gnome-software process and flatpak system helper process. + * Ideally this should be set when needed in the flatpak plugin, but + * umask is thread-unsafe so there is really no local way to fix this. + */ + umask (022); + + /* redirect logs */ + application = gs_application_new (); + appinfo = g_desktop_app_info_new ("org.gnome.Software.desktop"); + g_set_application_name (g_app_info_get_name (G_APP_INFO (appinfo))); + status = g_application_run (G_APPLICATION (application), argc, argv); + return status; +} diff --git a/src/gs-metered-data-dialog.c b/src/gs-metered-data-dialog.c new file mode 100644 index 0000000..9cd6698 --- /dev/null +++ b/src/gs-metered-data-dialog.c @@ -0,0 +1,69 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright © 2020 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> + +#include "gs-metered-data-dialog.h" + +struct _GsMeteredDataDialog +{ + GtkDialog parent_instance; + + GtkWidget *button_network_settings; +}; + +G_DEFINE_TYPE (GsMeteredDataDialog, gs_metered_data_dialog, GTK_TYPE_DIALOG) + +static void +button_network_settings_clicked_cb (GtkButton *button, + gpointer user_data) +{ + g_autoptr(GError) error_local = NULL; + + if (!g_spawn_command_line_async ("gnome-control-center wifi", &error_local)) { + g_warning ("Error opening GNOME Control Center: %s", + error_local->message); + return; + } +} + +static void +gs_metered_data_dialog_init (GsMeteredDataDialog *dialog) +{ + gtk_widget_init_template (GTK_WIDGET (dialog)); +} + +static void +gs_metered_data_dialog_class_init (GsMeteredDataDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-metered-data-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsMeteredDataDialog, button_network_settings); + + gtk_widget_class_bind_template_callback (widget_class, button_network_settings_clicked_cb); +} + +GtkWidget * +gs_metered_data_dialog_new (GtkWindow *parent) +{ + GsMeteredDataDialog *dialog; + + dialog = g_object_new (GS_TYPE_METERED_DATA_DIALOG, + "use-header-bar", TRUE, + "transient-for", parent, + "modal", TRUE, + NULL); + + return GTK_WIDGET (dialog); +} diff --git a/src/gs-metered-data-dialog.h b/src/gs-metered-data-dialog.h new file mode 100644 index 0000000..8219e9d --- /dev/null +++ b/src/gs-metered-data-dialog.h @@ -0,0 +1,23 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright © 2020 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_METERED_DATA_DIALOG (gs_metered_data_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsMeteredDataDialog, gs_metered_data_dialog, GS, METERED_DATA_DIALOG, GtkDialog) + +GtkWidget *gs_metered_data_dialog_new (GtkWindow *parent); + +G_END_DECLS diff --git a/src/gs-metered-data-dialog.ui b/src/gs-metered-data-dialog.ui new file mode 100644 index 0000000..def0243 --- /dev/null +++ b/src/gs-metered-data-dialog.ui @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsMeteredDataDialog" parent="GtkDialog"> + <property name="title" translatable="yes">Automatic Updates Paused</property> + <property name="modal">True</property> + <property name="destroy-with-parent">True</property> + <property name="resizable">False</property> + <property name="type-hint">dialog</property> + <property name="skip-taskbar-hint">True</property> + <property name="use-header-bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <child type="title"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes">Automatic Updates Paused</property> + <property name="selectable">False</property> + <style> + <class name="title"/> + </style> + </object> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="margin">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="justify">fill</property> + <property name="wrap">True</property> + <property name="wrap-mode">word-char</property> + <property name="label" translatable="yes">The current network is metered. Metered connections have data limits or charges associated with them. To save data, automatic updates have therefore been paused. + +Automatic updates will be resumed when an unmetered network becomes available. Until then, it is still possible to manually install updates. + +Alternatively, if the current network has been incorrectly identified as being metered, this setting can be changed.</property> + <property name="max-width-chars">40</property> + <property name="halign">center</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <child type="center"> + <object class="GtkButton" id="button_network_settings"> + <property name="visible">True</property> + <property name="label" translatable="yes">Open Network _Settings</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="use-underline">True</property> + <signal name="clicked" handler="button_network_settings_clicked_cb"/> + </object> + </child> + </object> + <packing> + <property name="fill">False</property> + <property name="expand">False</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-moderate-page.c b/src/gs-moderate-page.c new file mode 100644 index 0000000..edd77aa --- /dev/null +++ b/src/gs-moderate-page.c @@ -0,0 +1,340 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2016-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> + +#include "gs-app-row.h" +#include "gs-review-row.h" +#include "gs-shell.h" +#include "gs-moderate-page.h" +#include "gs-common.h" + +struct _GsModeratePage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GCancellable *cancellable; + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + GsShell *shell; + + GtkWidget *list_box_install; + GtkWidget *scrolledwindow_install; + GtkWidget *spinner_install; + GtkWidget *stack_install; +}; + +G_DEFINE_TYPE (GsModeratePage, gs_moderate_page, GS_TYPE_PAGE) + +static void +gs_moderate_page_app_set_review_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to set review: %s", error->message); + return; + } +} + +static void +gs_moderate_page_perhaps_hide_app_row (GsModeratePage *self, GsApp *app) +{ + GsAppRow *app_row = NULL; + gboolean is_visible = FALSE; + g_autoptr(GList) children = NULL; + + children = gtk_container_get_children (GTK_CONTAINER (self->list_box_install)); + for (GList *l = children; l != NULL; l = l->next) { + GtkWidget *w = GTK_WIDGET (l->data); + if (!gtk_widget_get_visible (w)) + continue; + if (GS_IS_APP_ROW (w)) { + GsApp *app_tmp = gs_app_row_get_app (GS_APP_ROW (w)); + if (g_strcmp0 (gs_app_get_id (app), + gs_app_get_id (app_tmp)) == 0) { + app_row = GS_APP_ROW (w); + continue; + } + } + if (GS_IS_REVIEW_ROW (w)) { + GsApp *app_tmp = g_object_get_data (G_OBJECT (w), "GsApp"); + if (g_strcmp0 (gs_app_get_id (app), + gs_app_get_id (app_tmp)) == 0) { + is_visible = TRUE; + break; + } + } + } + if (!is_visible && app_row != NULL) + gs_app_row_unreveal (app_row); +} + +static void +gs_moderate_page_review_clicked_cb (GsReviewRow *row, + GsPluginAction action, + GsModeratePage *self) +{ + GsApp *app = g_object_get_data (G_OBJECT (row), "GsApp"); + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (action, + "interactive", TRUE, + "app", app, + "review", gs_review_row_get_review (row), + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_moderate_page_app_set_review_cb, + self); + gtk_widget_set_visible (GTK_WIDGET (row), FALSE); + + /* if there are no more visible rows, hide the app */ + gs_moderate_page_perhaps_hide_app_row (self, app); +} + +static void +gs_moderate_page_selection_changed_cb (GtkListBox *listbox, + GsAppRow *app_row, + GsModeratePage *self) +{ + g_autofree gchar *tmp = NULL; + tmp = gs_app_to_string (gs_app_row_get_app (app_row)); + g_print ("%s", tmp); +} + +static void +gs_moderate_page_add_app (GsModeratePage *self, GsApp *app) +{ + GPtrArray *reviews; + GtkWidget *app_row; + guint i; + + /* this hides the action button */ + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + + /* add top level app */ + app_row = gs_app_row_new (app); + gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE); + gtk_container_add (GTK_CONTAINER (self->list_box_install), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button); + + /* add reviews */ + reviews = gs_app_get_reviews (app); + for (i = 0; i < reviews->len; i++) { + AsReview *review = g_ptr_array_index (reviews, i); + GtkWidget *row = gs_review_row_new (review); + gtk_widget_set_margin_start (row, 250); + gtk_widget_set_margin_end (row, 250); + gs_review_row_set_actions (GS_REVIEW_ROW (row), + 1 << GS_PLUGIN_ACTION_REVIEW_UPVOTE | + 1 << GS_PLUGIN_ACTION_REVIEW_DOWNVOTE | + 1 << GS_PLUGIN_ACTION_REVIEW_DISMISS | + 1 << GS_PLUGIN_ACTION_REVIEW_REPORT); + g_signal_connect (row, "button-clicked", + G_CALLBACK (gs_moderate_page_review_clicked_cb), self); + g_object_set_data_full (G_OBJECT (row), "GsApp", + g_object_ref (app), + (GDestroyNotify) g_object_unref); + gtk_container_add (GTK_CONTAINER (self->list_box_install), row); + } + gtk_widget_show (app_row); +} + +static void +gs_moderate_page_get_unvoted_reviews_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + guint i; + GsApp *app; + GsModeratePage *self = GS_MODERATE_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + gs_stop_spinner (GTK_SPINNER (self->spinner_install)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "view"); + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get moderate apps: %s", error->message); + return; + } + + /* no results */ + if (gs_app_list_length (list) == 0) { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), + "uptodate"); + return; + } + + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_moderate_page_add_app (self, app); + } +} + +static void +gs_moderate_page_load (GsModeratePage *self) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* remove old entries */ + gs_container_remove_all (GTK_CONTAINER (self->list_box_install)); + + /* get unvoted reviews as apps */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_moderate_page_get_unvoted_reviews_cb, + self); + gs_start_spinner (GTK_SPINNER (self->spinner_install)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "spinner"); +} + +static void +gs_moderate_page_reload (GsPage *page) +{ + GsModeratePage *self = GS_MODERATE_PAGE (page); + if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_MODERATE) + gs_moderate_page_load (self); +} + +static void +gs_moderate_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsModeratePage *self = GS_MODERATE_PAGE (page); + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_MODERATE) { + g_warning ("Called switch_to(moderate) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_MODERATE) + gs_grab_focus_when_mapped (self->scrolledwindow_install); + gs_moderate_page_load (self); +} + +static void +gs_moderate_page_list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header; + gtk_list_box_row_set_header (row, NULL); + if (before == NULL) + return; + if (GS_IS_REVIEW_ROW (before) && GS_IS_APP_ROW (row)) { + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); + } +} + +static gboolean +gs_moderate_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsModeratePage *self = GS_MODERATE_PAGE (page); + g_return_val_if_fail (GS_IS_MODERATE_PAGE (self), TRUE); + + self->shell = shell; + self->plugin_loader = g_object_ref (plugin_loader); + self->cancellable = g_object_ref (cancellable); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_install), + gs_moderate_page_list_header_func, + self, NULL); + return TRUE; +} + +static void +gs_moderate_page_dispose (GObject *object) +{ + GsModeratePage *self = GS_MODERATE_PAGE (object); + + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + + G_OBJECT_CLASS (gs_moderate_page_parent_class)->dispose (object); +} + +static void +gs_moderate_page_class_init (GsModeratePageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_moderate_page_dispose; + page_class->switch_to = gs_moderate_page_switch_to; + page_class->reload = gs_moderate_page_reload; + page_class->setup = gs_moderate_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-moderate-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsModeratePage, list_box_install); + gtk_widget_class_bind_template_child (widget_class, GsModeratePage, scrolledwindow_install); + gtk_widget_class_bind_template_child (widget_class, GsModeratePage, spinner_install); + gtk_widget_class_bind_template_child (widget_class, GsModeratePage, stack_install); +} + +static void +gs_moderate_page_init (GsModeratePage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (self->list_box_install, "row-activated", + G_CALLBACK (gs_moderate_page_selection_changed_cb), self); + + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +} + +GsModeratePage * +gs_moderate_page_new (void) +{ + GsModeratePage *self; + self = g_object_new (GS_TYPE_MODERATE_PAGE, NULL); + return GS_MODERATE_PAGE (self); +} diff --git a/src/gs-moderate-page.h b/src/gs-moderate-page.h new file mode 100644 index 0000000..cce54d6 --- /dev/null +++ b/src/gs-moderate-page.h @@ -0,0 +1,22 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_MODERATE_PAGE (gs_moderate_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsModeratePage, gs_moderate_page, GS, MODERATE_PAGE, GsPage) + +GsModeratePage *gs_moderate_page_new (void); + +G_END_DECLS diff --git a/src/gs-moderate-page.ui b/src/gs-moderate-page.ui new file mode 100644 index 0000000..fafe6d4 --- /dev/null +++ b/src/gs-moderate-page.ui @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsModeratePage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="moderate-accessible"> + <property name="accessible-name" translatable="yes">Moderate page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack_install"> + <property name="visible">True</property> + <child> + <object class="GtkSpinner" id="spinner_install"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_install"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_install"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="vexpand">True</property> + <property name="shadow_type">none</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <child> + <object class="GtkListBox" id="list_box_install"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">view</property> + </packing> + </child> + + <child> + <object class="GtkBox" id="updates_uptodate_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">48</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child type="center"> + <object class="GtkBox" id="updates_uptodate_centerbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage" id="image_updates"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">object-select-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="label" translatable="yes">There are no reviews to moderate</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">uptodate</property> + </packing> + </child> + + </object> + </child> + </template> +</interface> diff --git a/src/gs-origin-popover-row.c b/src/gs-origin-popover-row.c new file mode 100644 index 0000000..43e1193 --- /dev/null +++ b/src/gs-origin-popover-row.c @@ -0,0 +1,202 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-origin-popover-row.h" + +#include <glib/gi18n.h> + +typedef struct +{ + GsApp *app; + GtkWidget *name_label; + GtkWidget *url_box; + GtkWidget *url_title; + GtkWidget *url_label; + GtkWidget *format_box; + GtkWidget *format_title; + GtkWidget *format_label; + GtkWidget *installation_box; + GtkWidget *installation_title; + GtkWidget *installation_label; + GtkWidget *branch_box; + GtkWidget *branch_title; + GtkWidget *branch_label; + GtkWidget *version_box; + GtkWidget *version_title; + GtkWidget *version_label; + GtkWidget *selected_image; +} GsOriginPopoverRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsOriginPopoverRow, gs_origin_popover_row, GTK_TYPE_LIST_BOX_ROW) + +static void +refresh_ui (GsOriginPopoverRow *row) +{ + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + g_autofree gchar *origin_ui = NULL; + g_autofree gchar *packaging_format = NULL; + g_autofree gchar *url = NULL; + + g_assert (GS_IS_ORIGIN_POPOVER_ROW (row)); + g_assert (GS_IS_APP (priv->app)); + + origin_ui = gs_app_get_origin_ui (priv->app); + if (origin_ui != NULL) { + gtk_label_set_text (GTK_LABEL (priv->name_label), origin_ui); + } + + if (gs_app_get_state (priv->app) == AS_APP_STATE_AVAILABLE_LOCAL) { + GFile *local_file = gs_app_get_local_file (priv->app); + url = g_file_get_basename (local_file); + /* TRANSLATORS: This is followed by a file name, e.g. "Name: gedit.rpm" */ + gtk_label_set_text (GTK_LABEL (priv->url_title), _("Name")); + } else { + url = g_strdup (gs_app_get_origin_hostname (priv->app)); + } + + if (url != NULL) { + gtk_label_set_text (GTK_LABEL (priv->url_label), url); + gtk_widget_show (priv->url_box); + } else { + gtk_widget_hide (priv->url_box); + } + + packaging_format = gs_app_get_packaging_format (priv->app); + if (packaging_format != NULL) { + gtk_label_set_text (GTK_LABEL (priv->format_label), packaging_format); + gtk_widget_show (priv->format_box); + } else { + gtk_widget_hide (priv->format_box); + } + + if (gs_app_get_bundle_kind (priv->app) == AS_BUNDLE_KIND_FLATPAK && + gs_app_get_scope (priv->app) != AS_APP_SCOPE_UNKNOWN) { + AsAppScope scope = gs_app_get_scope (priv->app); + if (scope == AS_APP_SCOPE_SYSTEM) { + /* TRANSLATORS: the installation location for flatpaks */ + gtk_label_set_text (GTK_LABEL (priv->installation_label), _("system")); + } else if (scope == AS_APP_SCOPE_USER) { + /* TRANSLATORS: the installation location for flatpaks */ + gtk_label_set_text (GTK_LABEL (priv->installation_label), _("user")); + } + gtk_widget_show (priv->installation_box); + } else { + gtk_widget_hide (priv->installation_box); + } + + if (gs_app_get_branch (priv->app) != NULL) { + gtk_label_set_text (GTK_LABEL (priv->branch_label), gs_app_get_branch (priv->app)); + gtk_widget_show (priv->branch_box); + } else { + gtk_widget_hide (priv->branch_box); + } + + if (gs_app_get_bundle_kind (priv->app) == AS_BUNDLE_KIND_SNAP) { + /* TRANSLATORS: the title for Snap channels */ + gtk_label_set_text (GTK_LABEL (priv->branch_title), _("Channel")); + gtk_label_set_text (GTK_LABEL (priv->version_label), gs_app_get_version (priv->app)); + gtk_widget_show (priv->version_box); + } else { + /* TRANSLATORS: the title for Flatpak branches */ + gtk_label_set_text (GTK_LABEL (priv->branch_title), _("Branch")); + gtk_widget_hide (priv->version_box); + } +} + +static void +gs_origin_popover_row_set_app (GsOriginPopoverRow *row, GsApp *app) +{ + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + + g_assert (priv->app == NULL); + + priv->app = g_object_ref (app); + refresh_ui (row); +} + +GsApp * +gs_origin_popover_row_get_app (GsOriginPopoverRow *row) +{ + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + return priv->app; +} + +void +gs_origin_popover_row_set_selected (GsOriginPopoverRow *row, gboolean selected) +{ + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + + gtk_widget_set_visible (priv->selected_image, selected); +} + +void +gs_origin_popover_row_set_size_group (GsOriginPopoverRow *row, GtkSizeGroup *size_group) +{ + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + + gtk_size_group_add_widget (size_group, priv->url_title); + gtk_size_group_add_widget (size_group, priv->format_title); + gtk_size_group_add_widget (size_group, priv->installation_title); + gtk_size_group_add_widget (size_group, priv->branch_title); + gtk_size_group_add_widget (size_group, priv->version_title); +} + +static void +gs_origin_popover_row_destroy (GtkWidget *object) +{ + GsOriginPopoverRow *row = GS_ORIGIN_POPOVER_ROW (object); + GsOriginPopoverRowPrivate *priv = gs_origin_popover_row_get_instance_private (row); + + g_clear_object (&priv->app); + + GTK_WIDGET_CLASS (gs_origin_popover_row_parent_class)->destroy (object); +} + +static void +gs_origin_popover_row_init (GsOriginPopoverRow *row) +{ + gtk_widget_init_template (GTK_WIDGET (row)); +} + +static void +gs_origin_popover_row_class_init (GsOriginPopoverRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = gs_origin_popover_row_destroy; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-origin-popover-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, name_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, url_box); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, url_title); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, url_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, format_box); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, format_title); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, format_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, installation_box); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, installation_title); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, installation_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, branch_box); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, branch_title); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, branch_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, version_box); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, version_title); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, version_label); + gtk_widget_class_bind_template_child_private (widget_class, GsOriginPopoverRow, selected_image); +} + +GtkWidget * +gs_origin_popover_row_new (GsApp *app) +{ + GsOriginPopoverRow *row = g_object_new (GS_TYPE_ORIGIN_POPOVER_ROW, NULL); + gs_origin_popover_row_set_app (row, app); + return GTK_WIDGET (row); +} diff --git a/src/gs-origin-popover-row.h b/src/gs-origin-popover-row.h new file mode 100644 index 0000000..2368b04 --- /dev/null +++ b/src/gs-origin-popover-row.h @@ -0,0 +1,32 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gnome-software-private.h" +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_ORIGIN_POPOVER_ROW (gs_origin_popover_row_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsOriginPopoverRow, gs_origin_popover_row, GS, ORIGIN_POPOVER_ROW, GtkListBoxRow) + +struct _GsOriginPopoverRowClass +{ + GtkListBoxRowClass parent_class; +}; + +GtkWidget *gs_origin_popover_row_new (GsApp *app); +GsApp *gs_origin_popover_row_get_app (GsOriginPopoverRow *row); +void gs_origin_popover_row_set_selected (GsOriginPopoverRow *row, + gboolean selected); +void gs_origin_popover_row_set_size_group (GsOriginPopoverRow *row, + GtkSizeGroup *size_group); + +G_END_DECLS diff --git a/src/gs-origin-popover-row.ui b/src/gs-origin-popover-row.ui new file mode 100644 index 0000000..3fd4eb0 --- /dev/null +++ b/src/gs-origin-popover-row.ui @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GsOriginPopoverRow" parent="GtkListBoxRow"> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="orientation">horizontal</property> + <property name="spacing">16</property> + <child> + <object class="GtkBox" id="vbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + </object> + </child> + <child> + <object class="GtkBox" id="url_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="url_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">URL</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="url_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="format_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="format_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: The packaging format of the app being installed, e.g. 'RPM' or 'Flatpak'">Format</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="format_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="installation_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="installation_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: The installation location for flatpaks, e.g. 'user' or 'system'">Installation</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="installation_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="branch_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="branch_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: The branch, e.g. 'stable' or '3.32'">Branch</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="branch_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="version_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="version_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: The available version of an app">Version</property> + <style> + <class name="app-row-origin-text"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="version_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="app-row-origin-text"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkImage" id="selected_image"> + <property name="visible">False</property> + <property name="pixel_size">16</property> + <property name="icon_name">object-select-symbolic</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-overview-page.c b/src/gs-overview-page.c new file mode 100644 index 0000000..36b6610 --- /dev/null +++ b/src/gs-overview-page.c @@ -0,0 +1,1061 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <math.h> + +#include "gs-shell.h" +#include "gs-overview-page.h" +#include "gs-app-list-private.h" +#include "gs-popular-tile.h" +#include "gs-feature-tile.h" +#include "gs-category-tile.h" +#include "gs-hiding-box.h" +#include "gs-common.h" + +#define N_TILES 9 +#define FEATURED_ROTATE_TIME 30 /* seconds */ + +typedef struct +{ + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + gboolean cache_valid; + GsShell *shell; + gint action_cnt; + gboolean loading_featured; + gboolean loading_popular; + gboolean loading_recent; + gboolean loading_popular_rotating; + gboolean loading_categories; + gboolean empty; + gchar *category_of_day; + GHashTable *category_hash; /* id : GsCategory */ + GSettings *settings; + GsApp *third_party_repo; + guint featured_rotate_timer_id; + + GtkWidget *infobar_third_party; + GtkWidget *label_third_party; + GtkWidget *overlay; + GtkWidget *stack_featured; + GtkWidget *button_featured_back; + GtkWidget *button_featured_forwards; + GtkWidget *box_overview; + GtkWidget *box_popular; + GtkWidget *box_popular_rotating; + GtkWidget *box_recent; + GtkWidget *category_heading; + GtkWidget *flowbox_categories; + GtkWidget *popular_heading; + GtkWidget *recent_heading; + GtkWidget *scrolledwindow_overview; + GtkWidget *stack_overview; +} GsOverviewPagePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsOverviewPage, gs_overview_page, GS_TYPE_PAGE) + +enum { + SIGNAL_REFRESHED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +typedef struct { + GsCategory *category; + GsOverviewPage *self; + const gchar *title; +} LoadData; + +static void +load_data_free (LoadData *data) +{ + if (data->category != NULL) + g_object_unref (data->category); + if (data->self != NULL) + g_object_unref (data->self); + g_slice_free (LoadData, data); +} + +static void +gs_overview_page_invalidate (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + priv->cache_valid = FALSE; +} + +static void +app_tile_clicked (GsAppTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsApp *app; + + app = gs_app_tile_get_app (tile); + gs_shell_show_app (priv->shell, app); +} + +static gboolean +filter_category (GsApp *app, gpointer user_data) +{ + const gchar *category = (const gchar *) user_data; + + return !gs_app_has_category (app, category); +} + +static void +gs_overview_page_decrement_action_cnt (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + /* every job increments this */ + if (priv->action_cnt == 0) { + g_warning ("action_cnt already zero!"); + return; + } + if (--priv->action_cnt > 0) + return; + + /* all done */ + priv->cache_valid = TRUE; + g_signal_emit (self, signals[SIGNAL_REFRESHED], 0); + priv->loading_categories = FALSE; + priv->loading_featured = FALSE; + priv->loading_popular = FALSE; + priv->loading_recent = FALSE; + priv->loading_popular_rotating = FALSE; +} + +static void +gs_overview_page_get_popular_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get popular apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get popular apps: %s", error->message); + goto out; + } + + /* not enough to show */ + if (gs_app_list_length (list) < N_TILES) { + g_warning ("Only %u apps for popular list, hiding", + gs_app_list_length (list)); + gtk_widget_set_visible (priv->box_popular, FALSE); + gtk_widget_set_visible (priv->popular_heading, FALSE); + goto out; + } + + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_randomize (list); + + gs_container_remove_all (GTK_CONTAINER (priv->box_popular)); + + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->box_popular), tile); + } + gtk_widget_set_visible (priv->box_popular, TRUE); + gtk_widget_set_visible (priv->popular_heading, TRUE); + + priv->empty = FALSE; + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +gs_overview_page_get_recent_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get recent apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get recent apps: %s", error->message); + goto out; + } + + /* not enough to show */ + if (gs_app_list_length (list) < N_TILES) { + g_warning ("Only %u apps for recent list, hiding", + gs_app_list_length (list)); + gtk_widget_set_visible (priv->box_recent, FALSE); + gtk_widget_set_visible (priv->recent_heading, FALSE); + goto out; + } + + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_randomize (list); + + gs_container_remove_all (GTK_CONTAINER (priv->box_recent)); + + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->box_recent), tile); + } + gtk_widget_set_visible (priv->box_recent, TRUE); + gtk_widget_set_visible (priv->recent_heading, TRUE); + + priv->empty = FALSE; + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +gs_overview_page_category_more_cb (GtkButton *button, GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsCategory *cat; + const gchar *id; + + id = g_object_get_data (G_OBJECT (button), "GnomeSoftware::CategoryId"); + if (id == NULL) + return; + cat = g_hash_table_lookup (priv->category_hash, id); + if (cat == NULL) + return; + gs_shell_show_category (priv->shell, cat); +} + +static void +gs_overview_page_get_category_apps_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + LoadData *load_data = (LoadData *) user_data; + GsOverviewPage *self = load_data->self; + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsApp *app; + GtkWidget *box; + GtkWidget *button; + GtkWidget *headerbox; + GtkWidget *label; + GtkWidget *tile; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get popular apps */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + goto out; + g_warning ("failed to get category %s featured applications: %s", + gs_category_get_id (load_data->category), + error->message); + goto out; + } else if (gs_app_list_length (list) < N_TILES) { + g_warning ("hiding category %s featured applications: " + "found only %u to show, need at least %d", + gs_category_get_id (load_data->category), + gs_app_list_length (list), N_TILES); + goto out; + } + gs_app_list_randomize (list); + + /* add header */ + headerbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 9); + gtk_widget_set_visible (headerbox, TRUE); + + /* add label */ + label = gtk_label_new (load_data->title); + gtk_widget_set_visible (label, TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.f); + gtk_widget_set_margin_top (label, 24); + gtk_widget_set_margin_bottom (label, 6); + gtk_widget_set_hexpand (label, TRUE); + gtk_style_context_add_class (gtk_widget_get_style_context (label), + "index-title-alignment-software"); + gtk_container_add (GTK_CONTAINER (headerbox), label); + + /* add button */ + button = gtk_button_new_with_label (_("More…")); + gtk_style_context_add_class (gtk_widget_get_style_context (button), + "overview-more-button"); + g_object_set_data_full (G_OBJECT (button), "GnomeSoftware::CategoryId", + g_strdup (gs_category_get_id (load_data->category)), + g_free); + gtk_widget_set_visible (button, TRUE); + gtk_widget_set_valign (button, GTK_ALIGN_END); + gtk_widget_set_margin_bottom (button, 9); + g_signal_connect (button, "clicked", + G_CALLBACK (gs_overview_page_category_more_cb), self); + gtk_container_add (GTK_CONTAINER (headerbox), button); + gtk_container_add (GTK_CONTAINER (priv->box_popular_rotating), headerbox); + + /* add hiding box */ + box = gs_hiding_box_new (); + gs_hiding_box_set_spacing (GS_HIDING_BOX (box), 14); + gtk_widget_set_visible (box, TRUE); + gtk_widget_set_valign (box, GTK_ALIGN_START); + gtk_container_add (GTK_CONTAINER (priv->box_popular_rotating), box); + + /* add all the apps */ + for (i = 0; i < gs_app_list_length (list) && i < N_TILES; i++) { + app = gs_app_list_index (list, i); + tile = gs_popular_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (box), tile); + } + + priv->empty = FALSE; + +out: + load_data_free (load_data); + gs_overview_page_decrement_action_cnt (self); +} + +static void +_feature_banner_forward (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *visible_child; + GtkWidget *next_child = NULL; + GList *banner_link; + g_autoptr(GList) banners = NULL; + + visible_child = gtk_stack_get_visible_child (GTK_STACK (priv->stack_featured)); + banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); + if (banners == NULL) + return; + + /* find banner after the currently visible one */ + for (banner_link = banners; banner_link != NULL; banner_link = banner_link->next) { + GtkWidget *child = banner_link->data; + if (child == visible_child) { + if (banner_link->next != NULL) + next_child = banner_link->next->data; + break; + } + } + if (next_child == NULL) + next_child = g_list_first(banners)->data; + gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), next_child); +} + +static void +_feature_banner_back (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *visible_child; + GtkWidget *next_child = NULL; + GList *banner_link; + g_autoptr(GList) banners = NULL; + + visible_child = gtk_stack_get_visible_child (GTK_STACK (priv->stack_featured)); + banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); + if (banners == NULL) + return; + + /* find banner before the currently visible one */ + for (banner_link = banners; banner_link != NULL; banner_link = banner_link->next) { + GtkWidget *child = banner_link->data; + if (child == visible_child) { + if (banner_link->prev != NULL) + next_child = banner_link->prev->data; + break; + } + } + if (next_child == NULL) + next_child = g_list_last(banners)->data; + gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), next_child); +} + +static gboolean +gs_overview_page_featured_rotate_cb (gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + _feature_banner_forward (self); + return G_SOURCE_CONTINUE; +} + +static void +featured_reset_rotate_timer (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + if (priv->featured_rotate_timer_id != 0) + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = g_timeout_add_seconds (FEATURED_ROTATE_TIME, + gs_overview_page_featured_rotate_cb, + self); +} + +static void +_featured_back_clicked_cb (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + _feature_banner_back (self); +} + +static void +_featured_forward_clicked_cb (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + _feature_banner_forward (self); +} + +static void +gs_overview_page_get_featured_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + goto out; + + if (priv->featured_rotate_timer_id != 0) { + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = 0; + } + + gs_container_remove_all (GTK_CONTAINER (priv->stack_featured)); + gtk_widget_set_visible (priv->overlay, gs_app_list_length (list) > 0); + gtk_widget_set_visible (priv->button_featured_back, gs_app_list_length (list) > 1); + gtk_widget_set_visible (priv->button_featured_forwards, gs_app_list_length (list) > 1); + if (list == NULL) { + g_warning ("failed to get featured apps: %s", + error->message); + goto out; + } + if (gs_app_list_length (list) == 0) { + g_warning ("failed to get featured apps: " + "no apps to show"); + goto out; + } + + if (g_getenv ("GNOME_SOFTWARE_FEATURED") == NULL) { + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID); + gs_app_list_randomize (list); + } + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GtkWidget *tile = gs_feature_tile_new (app); + g_signal_connect (tile, "clicked", + G_CALLBACK (app_tile_clicked), self); + gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + } + + priv->empty = FALSE; + featured_reset_rotate_timer (self); + +out: + gs_overview_page_decrement_action_cnt (self); +} + +static void +category_tile_clicked (GsCategoryTile *tile, gpointer data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsCategory *category; + + category = gs_category_tile_get_category (tile); + gs_shell_show_category (priv->shell, category); +} + +static void +gs_overview_page_get_categories_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + guint i; + GsCategory *cat; + GtkFlowBox *flowbox; + GtkWidget *tile; + guint added_cnt = 0; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) list = NULL; + + list = gs_plugin_loader_job_get_categories_finish (plugin_loader, res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get categories: %s", error->message); + goto out; + } + gs_container_remove_all (GTK_CONTAINER (priv->flowbox_categories)); + + /* add categories to the correct flowboxes, the second being hidden */ + for (i = 0; i < list->len; i++) { + cat = GS_CATEGORY (g_ptr_array_index (list, i)); + if (gs_category_get_size (cat) == 0) + continue; + tile = gs_category_tile_new (cat); + g_signal_connect (tile, "clicked", + G_CALLBACK (category_tile_clicked), self); + flowbox = GTK_FLOW_BOX (priv->flowbox_categories); + gtk_flow_box_insert (flowbox, tile, -1); + gtk_widget_set_can_focus (gtk_widget_get_parent (tile), FALSE); + added_cnt++; + + /* we save these for the 'More...' buttons */ + g_hash_table_insert (priv->category_hash, + g_strdup (gs_category_get_id (cat)), + g_object_ref (cat)); + } + +out: + if (added_cnt > 0) + priv->empty = FALSE; + gtk_widget_set_visible (priv->category_heading, added_cnt > 0); + + gs_overview_page_decrement_action_cnt (self); +} + +static const gchar * +gs_overview_page_get_category_label (const gchar *id) +{ + if (g_strcmp0 (id, "audio-video") == 0) { + /* TRANSLATORS: this is a heading for audio applications which + * have been featured ('recommended') by the distribution */ + return _("Recommended Audio & Video Applications"); + } + if (g_strcmp0 (id, "games") == 0) { + /* TRANSLATORS: this is a heading for games which have been + * featured ('recommended') by the distribution */ + return _("Recommended Games"); + } + if (g_strcmp0 (id, "graphics") == 0) { + /* TRANSLATORS: this is a heading for graphics applications + * which have been featured ('recommended') by the distribution */ + return _("Recommended Graphics Applications"); + } + if (g_strcmp0 (id, "productivity") == 0) { + /* TRANSLATORS: this is a heading for office applications which + * have been featured ('recommended') by the distribution */ + return _("Recommended Productivity Applications"); + } + return NULL; +} + +static GPtrArray * +gs_overview_page_get_random_categories (void) +{ + GPtrArray *cats; + guint i; + g_autoptr(GDateTime) date = NULL; + g_autoptr(GRand) rand = NULL; + const gchar *ids[] = { "audio-video", + "games", + "graphics", + "productivity", + NULL }; + + date = g_date_time_new_now_utc (); + rand = g_rand_new_with_seed ((guint32) g_date_time_get_day_of_year (date)); + cats = g_ptr_array_new_with_free_func (g_free); + for (i = 0; ids[i] != NULL; i++) + g_ptr_array_add (cats, g_strdup (ids[i])); + for (i = 0; i < powl (cats->len + 1, 2); i++) { + gpointer tmp; + guint rnd1 = (guint) g_rand_int_range (rand, 0, (gint32) cats->len); + guint rnd2 = (guint) g_rand_int_range (rand, 0, (gint32) cats->len); + if (rnd1 == rnd2) + continue; + tmp = cats->pdata[rnd1]; + cats->pdata[rnd1] = cats->pdata[rnd2]; + cats->pdata[rnd2] = tmp; + } + for (i = 0; i < cats->len; i++) { + const gchar *tmp = g_ptr_array_index (cats, i); + g_debug ("%u = %s", i + 1, tmp); + } + return cats; +} + +static void +refresh_third_party_repo (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + /* only show if never prompted and third party repo is available */ + if (g_settings_get_boolean (priv->settings, "show-nonfree-prompt") && + priv->third_party_repo != NULL && + gs_app_get_state (priv->third_party_repo) == AS_APP_STATE_AVAILABLE) { + gtk_widget_set_visible (priv->infobar_third_party, TRUE); + } else { + gtk_widget_set_visible (priv->infobar_third_party, FALSE); + } +} + +static void +resolve_third_party_repo_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("resolve third party repo cancelled"); + return; + } else { + g_warning ("failed to resolve third party repo: %s", error->message); + return; + } + } + + /* save results for later */ + g_clear_object (&priv->third_party_repo); + if (gs_app_list_length (list) > 0) + priv->third_party_repo = g_object_ref (gs_app_list_index (list, 0)); + + /* refresh widget */ + refresh_third_party_repo (self); +} + +static gboolean +is_fedora (void) +{ + const gchar *id = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + os_release = gs_os_release_new (NULL); + if (os_release == NULL) + return FALSE; + + id = gs_os_release_get_id (os_release); + if (g_strcmp0 (id, "fedora") == 0) + return TRUE; + + return FALSE; +} + +static void +reload_third_party_repo (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + const gchar *third_party_repo_package = "fedora-workstation-repositories"; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* only show if never prompted */ + if (!g_settings_get_boolean (priv->settings, "show-nonfree-prompt")) + return; + + /* Fedora-specific functionality */ + if (!is_fedora ()) + return; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES, + "search", third_party_repo_package, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + priv->cancellable, + (GAsyncReadyCallback) resolve_third_party_repo_cb, + self); +} + +static void +gs_overview_page_load (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + guint i; + + priv->empty = TRUE; + + if (!priv->loading_featured) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_featured = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_FEATURED, + "max-results", 5, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_featured_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_popular) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_popular = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR, + "max-results", 20, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_popular_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_recent) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + priv->loading_recent = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_RECENT, + "age", (guint64) (60 * 60 * 24 * 60), + "max-results", 20, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_recent_cb, + self); + priv->action_cnt++; + } + + if (!priv->loading_popular_rotating) { + const guint MAX_CATS = 2; + g_autoptr(GPtrArray) cats_random = NULL; + cats_random = gs_overview_page_get_random_categories (); + + /* remove existing widgets, if any */ + gs_container_remove_all (GTK_CONTAINER (priv->box_popular_rotating)); + + /* load all the categories */ + for (i = 0; i < cats_random->len && i < MAX_CATS; i++) { + LoadData *load_data; + const gchar *cat_id; + g_autoptr(GsCategory) category = NULL; + g_autoptr(GsCategory) featured_category = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + cat_id = g_ptr_array_index (cats_random, i); + if (i == 0) { + g_free (priv->category_of_day); + priv->category_of_day = g_strdup (cat_id); + } + category = gs_category_new (cat_id); + featured_category = gs_category_new ("featured"); + gs_category_add_child (category, featured_category); + + load_data = g_slice_new0 (LoadData); + load_data->category = g_object_ref (category); + load_data->self = g_object_ref (self); + load_data->title = gs_overview_page_get_category_label (cat_id); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + "max-results", 20, + "category", featured_category, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + priv->cancellable, + gs_overview_page_get_category_apps_cb, + load_data); + priv->action_cnt++; + } + priv->loading_popular_rotating = TRUE; + } + + if (!priv->loading_categories) { + g_autoptr(GsPluginJob) plugin_job = NULL; + priv->loading_categories = TRUE; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORIES, NULL); + gs_plugin_loader_job_get_categories_async (priv->plugin_loader, plugin_job, + priv->cancellable, + gs_overview_page_get_categories_cb, + self); + priv->action_cnt++; + } + + reload_third_party_repo (self); +} + +static void +gs_overview_page_reload (GsPage *page) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + gs_overview_page_invalidate (self); + gs_overview_page_load (self); +} + +static void +gs_overview_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkWidget *widget; + GtkAdjustment *adj; + + if (gs_shell_get_mode (priv->shell) != GS_SHELL_MODE_OVERVIEW) { + g_warning ("Called switch_to(overview) when in mode %s", + gs_shell_get_mode_string (priv->shell)); + return; + } + + /* we hid the search bar */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_button")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); + + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "buttonbox_main")); + gtk_widget_show (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "menu_button")); + gtk_widget_show (widget); + + if (scroll_up) { + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolledwindow_overview)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } + + gs_grab_focus_when_mapped (priv->scrolledwindow_overview); + + if (priv->cache_valid || priv->action_cnt > 0) + return; + gs_overview_page_load (self); +} + +static void +third_party_response_cb (GtkInfoBar *info_bar, + gint response_id, + GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + g_settings_set_boolean (priv->settings, "show-nonfree-prompt", FALSE); + if (response_id == GTK_RESPONSE_CLOSE) { + gtk_widget_hide (priv->infobar_third_party); + return; + } + if (response_id != GTK_RESPONSE_YES) + return; + + if (gs_app_get_state (priv->third_party_repo) == AS_APP_STATE_AVAILABLE) { + gs_page_install_app (GS_PAGE (self), priv->third_party_repo, + GS_SHELL_INTERACTION_FULL, + priv->cancellable); + } + + refresh_third_party_repo (self); +} + +static gboolean +gs_overview_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (page); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GtkAdjustment *adj; + GtkWidget *tile; + gint i; + g_autoptr(GString) str = g_string_new (NULL); + + g_return_val_if_fail (GS_IS_OVERVIEW_PAGE (self), TRUE); + + priv->plugin_loader = g_object_ref (plugin_loader); + priv->builder = g_object_ref (builder); + priv->cancellable = g_object_ref (cancellable); + priv->category_hash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); + + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Access additional software from selected third party sources.")); + g_string_append (str, " "); + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Some of this software is proprietary and therefore has restrictions on use, sharing, and access to source code.")); + g_string_append_printf (str, " <a href=\"%s\">%s</a>", + "https://fedoraproject.org/wiki/Workstation/Third_Party_Software_Repositories", + /* TRANSLATORS: this is the clickable + * link on the third party repositories info bar */ + _("Find out more…")); + gtk_label_set_markup (GTK_LABEL (priv->label_third_party), str->str); + + /* create info bar if not already dismissed in initial-setup */ + refresh_third_party_repo (self); + reload_third_party_repo (self); + gtk_info_bar_add_button (GTK_INFO_BAR (priv->infobar_third_party), + /* TRANSLATORS: button to turn on third party software repositories */ + _("Enable"), GTK_RESPONSE_YES); + g_signal_connect (priv->infobar_third_party, "response", + G_CALLBACK (third_party_response_cb), self); + + /* avoid a ref cycle */ + priv->shell = shell; + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolledwindow_overview)); + gtk_container_set_focus_vadjustment (GTK_CONTAINER (priv->box_overview), adj); + + tile = gs_feature_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + + for (i = 0; i < N_TILES; i++) { + tile = gs_popular_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->box_popular), tile); + } + + for (i = 0; i < N_TILES; i++) { + tile = gs_popular_tile_new (NULL); + gtk_container_add (GTK_CONTAINER (priv->box_recent), tile); + } + + return TRUE; +} + +static void +gs_overview_page_init (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (priv->button_featured_back, "clicked", + G_CALLBACK (_featured_back_clicked_cb), self); + g_signal_connect (priv->button_featured_forwards, "clicked", + G_CALLBACK (_featured_forward_clicked_cb), self); + + priv->settings = g_settings_new ("org.gnome.software"); +} + +static void +gs_overview_page_dispose (GObject *object) +{ + GsOverviewPage *self = GS_OVERVIEW_PAGE (object); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + g_clear_object (&priv->builder); + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->cancellable); + g_clear_object (&priv->settings); + g_clear_object (&priv->third_party_repo); + g_clear_pointer (&priv->category_of_day, g_free); + g_clear_pointer (&priv->category_hash, g_hash_table_unref); + + if (priv->featured_rotate_timer_id != 0) { + g_source_remove (priv->featured_rotate_timer_id); + priv->featured_rotate_timer_id = 0; + } + + G_OBJECT_CLASS (gs_overview_page_parent_class)->dispose (object); +} + +static void +gs_overview_page_refreshed (GsOverviewPage *self) +{ + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + + if (priv->empty) { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack_overview), "no-results"); + } else { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack_overview), "overview"); + } +} + +static void +gs_overview_page_class_init (GsOverviewPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_overview_page_dispose; + page_class->switch_to = gs_overview_page_switch_to; + page_class->reload = gs_overview_page_reload; + page_class->setup = gs_overview_page_setup; + klass->refreshed = gs_overview_page_refreshed; + + signals [SIGNAL_REFRESHED] = + g_signal_new ("refreshed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsOverviewPageClass, refreshed), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-overview-page.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, infobar_third_party); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, label_third_party); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, overlay); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, stack_featured); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, button_featured_back); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, button_featured_forwards); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_overview); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular_rotating); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_recent); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, category_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, flowbox_categories); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, popular_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, recent_heading); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, scrolledwindow_overview); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, stack_overview); +} + +GsOverviewPage * +gs_overview_page_new (void) +{ + return GS_OVERVIEW_PAGE (g_object_new (GS_TYPE_OVERVIEW_PAGE, NULL)); +} diff --git a/src/gs-overview-page.h b/src/gs-overview-page.h new file mode 100644 index 0000000..6bd53eb --- /dev/null +++ b/src/gs-overview-page.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_OVERVIEW_PAGE (gs_overview_page_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsOverviewPage, gs_overview_page, GS, OVERVIEW_PAGE, GsPage) + +struct _GsOverviewPageClass +{ + GsPageClass parent_class; + + void (*refreshed) (GsOverviewPage *self); +}; + +GsOverviewPage *gs_overview_page_new (void); +void gs_overview_page_set_category (GsOverviewPage *self, + const gchar *category); + +G_END_DECLS diff --git a/src/gs-overview-page.ui b/src/gs-overview-page.ui new file mode 100644 index 0000000..e83bc17 --- /dev/null +++ b/src/gs-overview-page.ui @@ -0,0 +1,295 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsOverviewPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="overview-accessible"> + <property name="accessible-name" translatable="yes">Overview page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack_overview"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkInfoBar" id="infobar_third_party"> + <property name="visible">True</property> + <property name="app_paintable">True</property> + <property name="show_close_button">True</property> + <child internal-child="action_area"> + <object class="GtkButtonBox"> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child/> + </object> + </child> + <child internal-child="content_area"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="border_width">12</property> + <child> + <object class="GtkLabel" id="label_third_party_title"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Enable Third Party Software Repositories?</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label_third_party"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label">Provides access to additional software.</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_overview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkViewport" id="viewport_overview"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkBox" id="box_overview"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="hexpand">False</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="height-request">150</property> + <child type="overlay"> + <object class="GtkStack" id="stack_featured"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="transition-type">crossfade</property> + </object> + </child> + <child type="overlay"> + <object class="GtkButton" id="button_featured_back"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="can_focus">True</property> + <property name="halign">start</property> + <property name="valign">center</property> + <child internal-child="accessible"> + <object class="AtkObject" id="button_featured_back_accessible"> + <property name="accessible-name" translatable="yes">Previous</property> + </object> + </child> + <style> + <class name="osd"/> + <class name="featured-button-left"/> + </style> + <child> + <object class="GtkImage" id="back_image"> + <property name="visible">True</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + <child type="overlay"> + <object class="GtkButton" id="button_featured_forwards"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <child internal-child="accessible"> + <object class="AtkObject" id="button_featured_forwards_accessible"> + <property name="accessible-name" translatable="yes">Next</property> + </object> + </child> + <style> + <class name="osd"/> + <class name="featured-button-right"/> + </style> + <child> + <object class="GtkImage" id="forwards_image"> + <property name="visible">True</property> + <property name="icon_name">go-next-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkLabel" id="popular_heading"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is a heading for software which has been featured ('picked') by the distribution.">Editor’s Picks</property> + <property name="margin-top">21</property> + <property name="margin-bottom">6</property> + <accessibility> + <relation target="box_popular" type="label-for"/> + </accessibility> + <style> + <class name="index-title-alignment-software"/> + </style> + </object> + </child> + <child> + <object class="GsHidingBox" id="box_popular"> + <property name="visible">True</property> + <property name="spacing">14</property> + <property name="valign">start</property> + <accessibility> + <relation target="popular_heading" type="labelled-by"/> + </accessibility> + </object> + </child> + + <child> + <object class="GtkLabel" id="recent_heading"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: This is a heading for software which has been recently released upstream.">Recent Releases</property> + <property name="margin-top">21</property> + <property name="margin-bottom">6</property> + <accessibility> + <relation target="box_recent" type="label-for"/> + </accessibility> + <style> + <class name="index-title-alignment-software"/> + </style> + </object> + </child> + <child> + <object class="GsHidingBox" id="box_recent"> + <property name="visible">True</property> + <property name="spacing">14</property> + <property name="valign">start</property> + <accessibility> + <relation target="recent_heading" type="labelled-by"/> + </accessibility> + </object> + </child> + + <child> + <object class="GtkBox" id="box_popular_rotating"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="valign">start</property> + </object> + </child> + + <child> + <object class="GtkLabel" id="category_heading"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Categories</property> + <property name="margin-top">24</property> + <property name="margin-bottom">6</property> + <accessibility> + <relation target="flowbox_categories" type="label-for"/> + </accessibility> + <style> + <class name="index-title-alignment-software"/> + </style> + </object> + </child> + <child> + <object class="GtkFlowBox" id="flowbox_categories"> + <property name="visible">True</property> + <property name="margin-bottom">6</property> + <property name="row_spacing">14</property> + <property name="column_spacing">14</property> + <property name="homogeneous">True</property> + <property name="max_children_per_line">3</property> + <property name="selection_mode">none</property> + <accessibility> + <relation target="category_heading" type="labelled-by"/> + </accessibility> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">overview</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="noresults_grid_overview"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="row-spacing">12</property> + <property name="column-spacing">12</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="noappdata_icon"> + <property name="visible">True</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + <property name="pixel-size">64</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="noappdata_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">No Application Data Found</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="wrap">True</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="name">no-results</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-page.c b/src/gs-page.c new file mode 100644 index 0000000..ab5bdc6 --- /dev/null +++ b/src/gs-page.c @@ -0,0 +1,691 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-page.h" +#include "gs-common.h" +#include "gs-screenshot-image.h" + +typedef struct +{ + GsPluginLoader *plugin_loader; + GsShell *shell; + GtkWidget *header_start_widget; + GtkWidget *header_end_widget; + gboolean is_active; +} GsPagePrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GsPage, gs_page, GTK_TYPE_BIN) + +GsShell * +gs_page_get_shell (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + return priv->shell; +} + +typedef struct { + GsApp *app; + GsPage *page; + GCancellable *cancellable; + gulong notify_quirk_id; + GtkWidget *button_install; + GsPluginAction action; + GsShellInteraction interaction; +} GsPageHelper; + +static void +gs_page_helper_free (GsPageHelper *helper) +{ + if (helper->notify_quirk_id > 0) + g_signal_handler_disconnect (helper->app, helper->notify_quirk_id); + if (helper->app != NULL) + g_object_unref (helper->app); + if (helper->page != NULL) + g_object_unref (helper->page); + if (helper->cancellable != NULL) + g_object_unref (helper->cancellable); + g_slice_free (GsPageHelper, helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPageHelper, gs_page_helper_free); + +static void +gs_page_update_app_response_close_cb (GtkDialog *dialog, gint response, gpointer user_data) +{ + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +gs_page_show_update_message (GsPageHelper *helper, AsScreenshot *ss) +{ + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + GPtrArray *images; + GtkWidget *dialog; + g_autofree gchar *escaped = NULL; + + dialog = gtk_message_dialog_new (gs_shell_get_window (priv->shell), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", gs_app_get_name (helper->app)); + escaped = g_markup_escape_text (as_screenshot_get_caption (ss, NULL), -1); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", escaped); + + /* image is optional */ + images = as_screenshot_get_images (ss); + if (images->len) { + GtkWidget *content_area; + GtkWidget *ssimg; + g_autoptr(SoupSession) soup_session = NULL; + + /* load screenshot */ + soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, + gs_user_agent (), NULL); + ssimg = gs_screenshot_image_new (soup_session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), 400, 225); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), + helper->cancellable); + gtk_widget_set_margin_start (ssimg, 24); + gtk_widget_set_margin_end (ssimg, 24); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_container_add (GTK_CONTAINER (content_area), ssimg); + gtk_container_child_set (GTK_CONTAINER (content_area), ssimg, "pack-type", GTK_PACK_END, NULL); + } + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_update_app_response_close_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog)); +} + +static void +gs_page_app_installed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsPage *page = helper->page; + GsPagePrivate *priv = gs_page_get_instance_private (page); + gboolean ret; + g_autoptr(GError) error = NULL; + + ret = gs_plugin_loader_job_action_finish (plugin_loader, + res, + &error); + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("%s", error->message); + return; + } + if (!ret) { + g_warning ("failed to install %s: %s", + gs_app_get_id (helper->app), + error->message); + return; + } + + /* the single update needs system reboot, e.g. for firmware */ + if (gs_app_has_quirk (helper->app, GS_APP_QUIRK_NEEDS_REBOOT)) { + g_autoptr(GsAppList) list = gs_app_list_new (); + gs_app_list_add (list, helper->app); + gs_utils_reboot_notify (list); + } + + /* tell the user what they have to do */ + if (gs_app_get_kind (helper->app) == AS_APP_KIND_FIRMWARE && + gs_app_has_quirk (helper->app, GS_APP_QUIRK_NEEDS_USER_ACTION)) { + AsScreenshot *ss = gs_app_get_action_screenshot (helper->app); + if (ss != NULL && as_screenshot_get_caption (ss, NULL) != NULL) + gs_page_show_update_message (helper, ss); + } + + /* only show this if the window is not active */ + if (gs_app_is_installed (helper->app) && + helper->action == GS_PLUGIN_ACTION_INSTALL && + !gs_shell_is_active (priv->shell) && + ((helper->interaction) & GS_SHELL_INTERACTION_NOTIFY) != 0) + gs_app_notify_installed (helper->app); + + if (gs_app_is_installed (helper->app) && + GS_PAGE_GET_CLASS (page)->app_installed != NULL) { + GS_PAGE_GET_CLASS (page)->app_installed (page, helper->app); + } +} + +static void +gs_page_app_removed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsPage *page = helper->page; + gboolean ret; + g_autoptr(GError) error = NULL; + + ret = gs_plugin_loader_job_action_finish (plugin_loader, + res, + &error); + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("%s", error->message); + return; + } + if (!ret) { + g_warning ("failed to remove: %s", error->message); + return; + } + + if (!gs_app_is_installed (helper->app) && + GS_PAGE_GET_CLASS (page)->app_removed != NULL) { + GS_PAGE_GET_CLASS (page)->app_removed (page, helper->app); + } +} + +GtkWidget * +gs_page_get_header_start_widget (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + return priv->header_start_widget; +} + +void +gs_page_set_header_start_widget (GsPage *page, GtkWidget *widget) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_set_object (&priv->header_start_widget, widget); +} + +GtkWidget * +gs_page_get_header_end_widget (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + return priv->header_end_widget; +} + +void +gs_page_set_header_end_widget (GsPage *page, GtkWidget *widget) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_set_object (&priv->header_end_widget, widget); +} + +void +gs_page_install_app (GsPage *page, + GsApp *app, + GsShellInteraction interaction, + GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* probably non-free */ + if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) { + GtkResponseType response; + + response = gs_app_notify_unavailable (app, gs_shell_get_window (priv->shell)); + if (response != GTK_RESPONSE_OK) + return; + } + + helper = g_slice_new0 (GsPageHelper); + helper->action = GS_PLUGIN_ACTION_INSTALL; + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = g_object_ref (cancellable); + helper->interaction = interaction; + + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "app", helper->app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); +} + +static void +gs_page_update_app_response_cb (GtkDialog *dialog, + gint response, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + g_debug ("update %s", gs_app_get_id (helper->app)); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "interactive", TRUE, + "app", helper->app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); + g_steal_pointer (&helper); +} + +static void +gs_page_notify_quirk_cb (GsApp *app, GParamSpec *pspec, GsPageHelper *helper) +{ + gtk_widget_set_sensitive (helper->button_install, + !gs_app_has_quirk (helper->app, + GS_APP_QUIRK_NEEDS_USER_ACTION)); +} + +static void +gs_page_needs_user_action (GsPageHelper *helper, AsScreenshot *ss) +{ + GtkWidget *content_area; + GtkWidget *dialog; + g_autoptr(SoupSession) soup_session = NULL; + GtkWidget *ssimg; + g_autofree gchar *escaped = NULL; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + + dialog = gtk_message_dialog_new (gs_shell_get_window (priv->shell), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CANCEL, + /* TRANSLATORS: this is a prompt message, and + * '%s' is an application summary, e.g. 'GNOME Clocks' */ + _("Prepare %s"), + gs_app_get_name (helper->app)); + escaped = g_markup_escape_text (as_screenshot_get_caption (ss, NULL), -1); + gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog), + "%s", escaped); + + /* this will be enabled when the device is in the right mode */ + helper->button_install = gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: update the fw */ + _("Install"), + GTK_RESPONSE_OK); + helper->notify_quirk_id = + g_signal_connect (helper->app, "notify::quirk", + G_CALLBACK (gs_page_notify_quirk_cb), + helper); + gtk_widget_set_sensitive (helper->button_install, FALSE); + + /* load screenshot */ + soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, + gs_user_agent (), NULL); + ssimg = gs_screenshot_image_new (soup_session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), 400, 225); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), + helper->cancellable); + gtk_widget_set_margin_start (ssimg, 24); + gtk_widget_set_margin_end (ssimg, 24); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_container_add (GTK_CONTAINER (content_area), ssimg); + gtk_container_child_set (GTK_CONTAINER (content_area), ssimg, "pack-type", GTK_PACK_END, NULL); + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_update_app_response_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog)); +} + +void +gs_page_update_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* non-firmware applications do not have to be prepared */ + helper = g_slice_new0 (GsPageHelper); + helper->action = GS_PLUGIN_ACTION_UPDATE; + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = g_object_ref (cancellable); + + /* tell the user what they have to do */ + if (gs_app_get_kind (app) == AS_APP_KIND_FIRMWARE && + gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION)) { + AsScreenshot *ss = gs_app_get_action_screenshot (app); + if (ss != NULL && as_screenshot_get_caption (ss, NULL) != NULL) { + gs_page_needs_user_action (helper, ss); + return; + } + } + + /* generic fallback */ + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); +} + +static void +gs_page_remove_app_response_cb (GtkDialog *dialog, + gint response, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + g_debug ("remove %s", gs_app_get_id (helper->app)); + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "app", helper->app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_removed_cb, + helper); + g_steal_pointer (&helper); +} + +void +gs_page_remove_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + GtkWidget *dialog; + g_autofree gchar *message = NULL; + g_autofree gchar *title = NULL; + GtkWidget *remove_button; + GtkStyleContext *context; + + /* pending install */ + helper = g_slice_new0 (GsPageHelper); + helper->action = GS_PLUGIN_ACTION_REMOVE; + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = g_object_ref (cancellable); + if (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "interactive", TRUE, + "app", app, + NULL); + g_debug ("remove %s", gs_app_get_id (app)); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_removed_cb, + helper); + return; + } + + /* use different name and summary */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_SOURCE: + /* TRANSLATORS: this is a prompt message, and '%s' is an + * repository name, e.g. 'GNOME Nightly' */ + title = g_strdup_printf (_("Are you sure you want to remove " + "the %s repository?"), + gs_app_get_name (app)); + /* TRANSLATORS: longer dialog text */ + message = g_strdup_printf (_("All applications from %s will be " + "removed, and you will have to " + "re-install the repository to use them again."), + gs_app_get_name (app)); + break; + default: + /* TRANSLATORS: this is a prompt message, and '%s' is an + * application summary, e.g. 'GNOME Clocks' */ + title = g_strdup_printf (_("Are you sure you want to remove %s?"), + gs_app_get_name (app)); + /* TRANSLATORS: longer dialog text */ + message = g_strdup_printf (_("%s will be removed, and you will " + "have to install it to use it again."), + gs_app_get_name (app)); + break; + } + + /* ask for confirmation */ + dialog = gtk_message_dialog_new (gs_shell_get_window (priv->shell), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + "%s", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", message); + + /* TRANSLATORS: this is button text to remove the application */ + remove_button = gtk_dialog_add_button (GTK_DIALOG (dialog), _("Remove"), GTK_RESPONSE_OK); + context = gtk_widget_get_style_context (remove_button); + gtk_style_context_add_class (context, "destructive-action"); + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_remove_app_response_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog)); +} + +static void +gs_page_app_launched_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to launch GsApp: %s", error->message); + return; + } +} + +void +gs_page_launch_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + cancellable, + gs_page_app_launched_cb, + NULL); +} + +static void +gs_page_app_shortcut_added_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to add a shortcut to GsApp: %s", error->message); + return; + } +} + +void +gs_page_shortcut_add (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_ADD_SHORTCUT, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + cancellable, + gs_page_app_shortcut_added_cb, + NULL); +} + +static void +gs_page_app_shortcut_removed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to remove the shortcut to GsApp: %s", error->message); + return; + } +} + +void +gs_page_shortcut_remove (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE_SHORTCUT, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + cancellable, + gs_page_app_shortcut_removed_cb, + NULL); +} + +gboolean +gs_page_is_active (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_return_val_if_fail (GS_IS_PAGE (page), FALSE); + return priv->is_active; +} + +/** + * gs_page_switch_to: + * + * Pure virtual method that subclasses have to override to show page specific + * widgets. + */ +void +gs_page_switch_to (GsPage *page, + gboolean scroll_up) +{ + GsPageClass *klass = GS_PAGE_GET_CLASS (page); + GsPagePrivate *priv = gs_page_get_instance_private (page); + priv->is_active = TRUE; + if (klass->switch_to != NULL) + klass->switch_to (page, scroll_up); +} + +/** + * gs_page_switch_from: + * + * Pure virtual method that subclasses have to override to show page specific + * widgets. + */ +void +gs_page_switch_from (GsPage *page) +{ + GsPageClass *klass = GS_PAGE_GET_CLASS (page); + GsPagePrivate *priv = gs_page_get_instance_private (page); + priv->is_active = FALSE; + if (klass->switch_from != NULL) + klass->switch_from (page); +} + +void +gs_page_reload (GsPage *page) +{ + GsPageClass *klass; + g_return_if_fail (GS_IS_PAGE (page)); + klass = GS_PAGE_GET_CLASS (page); + if (klass->reload != NULL) + klass->reload (page); +} + +gboolean +gs_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsPageClass *klass; + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_return_val_if_fail (GS_IS_PAGE (page), FALSE); + + klass = GS_PAGE_GET_CLASS (page); + g_assert (klass->setup != NULL); + + priv->plugin_loader = g_object_ref (plugin_loader); + priv->shell = shell; + + return klass->setup (page, shell, plugin_loader, builder, cancellable, error); +} + +static void +gs_page_dispose (GObject *object) +{ + GsPage *page = GS_PAGE (object); + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->header_start_widget); + g_clear_object (&priv->header_end_widget); + + G_OBJECT_CLASS (gs_page_parent_class)->dispose (object); +} + +static void +gs_page_init (GsPage *page) +{ +} + +static void +gs_page_class_init (GsPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gs_page_dispose; +} + +GsPage * +gs_page_new (void) +{ + GsPage *page; + page = g_object_new (GS_TYPE_PAGE, NULL); + return GS_PAGE (page); +} diff --git a/src/gs-page.h b/src/gs-page.h new file mode 100644 index 0000000..9fc5ad8 --- /dev/null +++ b/src/gs-page.h @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-shell.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PAGE (gs_page_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPage, gs_page, GS, PAGE, GtkBin) + +struct _GsPageClass +{ + GtkBinClass parent_class; + + void (*app_installed) (GsPage *page, + GsApp *app); + void (*app_removed) (GsPage *page, + GsApp *app); + void (*switch_to) (GsPage *page, + gboolean scroll_up); + void (*switch_from) (GsPage *page); + void (*reload) (GsPage *page); + gboolean (*setup) (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error); +}; + +GsPage *gs_page_new (void); +GsShell *gs_page_get_shell (GsPage *page); +GtkWidget *gs_page_get_header_start_widget (GsPage *page); +void gs_page_set_header_start_widget (GsPage *page, + GtkWidget *widget); +GtkWidget *gs_page_get_header_end_widget (GsPage *page); +void gs_page_set_header_end_widget (GsPage *page, + GtkWidget *widget); +void gs_page_install_app (GsPage *page, + GsApp *app, + GsShellInteraction interaction, + GCancellable *cancellable); +void gs_page_remove_app (GsPage *page, + GsApp *app, + GCancellable *cancellable); +void gs_page_update_app (GsPage *page, + GsApp *app, + GCancellable *cancellable); +void gs_page_launch_app (GsPage *page, + GsApp *app, + GCancellable *cancellable); +void gs_page_shortcut_add (GsPage *page, + GsApp *app, + GCancellable *cancellable); +void gs_page_shortcut_remove (GsPage *page, + GsApp *app, + GCancellable *cancellable); +void gs_page_switch_to (GsPage *page, + gboolean scroll_up); +void gs_page_switch_from (GsPage *page); +void gs_page_reload (GsPage *page); +gboolean gs_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error); +gboolean gs_page_is_active (GsPage *page); + +G_END_DECLS diff --git a/src/gs-popular-tile.c b/src/gs-popular-tile.c new file mode 100644 index 0000000..0a7cb98 --- /dev/null +++ b/src/gs-popular-tile.c @@ -0,0 +1,136 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "gs-popular-tile.h" +#include "gs-star-widget.h" +#include "gs-common.h" + +struct _GsPopularTile +{ + GsAppTile parent_instance; + GtkWidget *label; + GtkWidget *image; + GtkWidget *eventbox; + GtkWidget *stack; + GtkWidget *stars; + GtkCssProvider *tile_provider; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPopularTile, gs_popular_tile, GS_TYPE_APP_TILE) + +static void +gs_popular_tile_dispose (GObject *object) +{ + GsPopularTile *tile = GS_POPULAR_TILE (object); + + g_clear_object (&tile->tile_provider); + + G_OBJECT_CLASS (gs_popular_tile_parent_class)->dispose (object); +} + +static void +gs_popular_tile_refresh (GsAppTile *self) +{ + GsPopularTile *tile = GS_POPULAR_TILE (self); + GsApp *app = gs_app_tile_get_app (self); + AtkObject *accessible; + gboolean installed; + g_autofree gchar *name = NULL; + const gchar *css; + + if (app == NULL) + return; + + accessible = gtk_widget_get_accessible (GTK_WIDGET (tile)); + + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + installed = TRUE; + /* TRANSLATORS: this refers to an app (by name) that is installed */ + name = g_strdup_printf (_("%s (Installed)"), + gs_app_get_name (app)); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_INSTALLING: + default: + installed = FALSE; + name = g_strdup (gs_app_get_name (app)); + break; + } + + gtk_widget_set_visible (tile->eventbox, installed); + + if (GTK_IS_ACCESSIBLE (accessible)) { + atk_object_set_name (accessible, name); + atk_object_set_description (accessible, gs_app_get_summary (app)); + } + + gtk_widget_set_sensitive (tile->stars, gs_app_get_rating (app) >= 0); + gs_star_widget_set_rating (GS_STAR_WIDGET (tile->stars), + gs_app_get_rating (app)); + gtk_stack_set_visible_child_name (GTK_STACK (tile->stack), "content"); + + /* perhaps set custom css */ + css = gs_app_get_metadata_item (app, "GnomeSoftware::PopularTile-css"); + gs_utils_widget_set_css (GTK_WIDGET (tile), &tile->tile_provider, "popular-tile", css); + + if (gs_app_get_pixbuf (app) != NULL) { + gs_image_set_from_pixbuf (GTK_IMAGE (tile->image), gs_app_get_pixbuf (app)); + } else { + gtk_image_set_from_icon_name (GTK_IMAGE (tile->image), + "application-x-executable", + GTK_ICON_SIZE_DIALOG); + } + + gtk_label_set_label (GTK_LABEL (tile->label), gs_app_get_name (app)); +} + +static void +gs_popular_tile_init (GsPopularTile *tile) +{ + gtk_widget_set_has_window (GTK_WIDGET (tile), FALSE); + gtk_widget_init_template (GTK_WIDGET (tile)); +} + +static void +gs_popular_tile_class_init (GsPopularTileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GsAppTileClass *app_tile_class = GS_APP_TILE_CLASS (klass); + + object_class->dispose = gs_popular_tile_dispose; + + app_tile_class->refresh = gs_popular_tile_refresh; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-popular-tile.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsPopularTile, label); + gtk_widget_class_bind_template_child (widget_class, GsPopularTile, image); + gtk_widget_class_bind_template_child (widget_class, GsPopularTile, eventbox); + gtk_widget_class_bind_template_child (widget_class, GsPopularTile, stack); + gtk_widget_class_bind_template_child (widget_class, GsPopularTile, stars); +} + +GtkWidget * +gs_popular_tile_new (GsApp *app) +{ + GsPopularTile *tile = g_object_new (GS_TYPE_POPULAR_TILE, NULL); + if (app != NULL) + gs_app_tile_set_app (GS_APP_TILE (tile), app); + return GTK_WIDGET (tile); +} diff --git a/src/gs-popular-tile.h b/src/gs-popular-tile.h new file mode 100644 index 0000000..7e7a8ff --- /dev/null +++ b/src/gs-popular-tile.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app-tile.h" + +G_BEGIN_DECLS + +#define GS_TYPE_POPULAR_TILE (gs_popular_tile_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPopularTile, gs_popular_tile, GS, POPULAR_TILE, GsAppTile) + +GtkWidget *gs_popular_tile_new (GsApp *app); + +G_END_DECLS diff --git a/src/gs-popular-tile.ui b/src/gs-popular-tile.ui new file mode 100644 index 0000000..17e4eec --- /dev/null +++ b/src/gs-popular-tile.ui @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsPopularTile" parent="GsAppTile"> + <property name="visible">True</property> + <style> + <class name="view"/> + <class name="tile"/> + </style> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="waiting"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="icon-name">content-loading-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="name">waiting</property> + </packing> + </child> + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <child type="overlay"> + <object class="GtkEventBox" id="eventbox"> + <property name="visible">False</property> + <property name="no_show_all">True</property> + <property name="visible_window">True</property> + <property name="halign">end</property> + <property name="valign">start</property> + <child> + <object class="GtkImage" id="installed-icon"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="margin-top">6</property> + <property name="margin-right">6</property> + <property name="icon-name">software-installed-symbolic</property> + <style> + <class name="installed-icon"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkEventBox" id="eboxbox"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-start">15</property> + <property name="margin-end">15</property> + <property name="margin-top">15</property> + <property name="margin-bottom">16</property> + <child> + <object class="GtkImage" id="image"> + <property name="width-request">64</property> + <property name="height-request">64</property> + <property name="visible">True</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <style> + <class name="icon-dropshadow"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="valign">end</property> + <property name="margin">6</property> + <property name="ellipsize">end</property> + <property name="width_chars">12</property> + <property name="max_width_chars">12</property> + </object> + </child> + <child> + <object class="GsStarWidget" id="stars"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="icon-size">12</property> + <style> + <class name="onlyjustvisible"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">content</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-prefs-dialog.c b/src/gs-prefs-dialog.c new file mode 100644 index 0000000..51191a7 --- /dev/null +++ b/src/gs-prefs-dialog.c @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-prefs-dialog.h" + +#include "gnome-software-private.h" +#include "gs-common.h" +#include "gs-os-release.h" +#include "gs-repo-row.h" +#include "gs-third-party-repo-row.h" +#include <glib/gi18n.h> + +struct _GsPrefsDialog +{ + GtkDialog parent_instance; + GSettings *settings; + + GCancellable *cancellable; + GsPluginLoader *plugin_loader; + GtkWidget *switch_updates; + GtkWidget *switch_updates_notify; +}; + +G_DEFINE_TYPE (GsPrefsDialog, gs_prefs_dialog, GTK_TYPE_DIALOG) + +static void +gs_prefs_dialog_dispose (GObject *object) +{ + GsPrefsDialog *dialog = GS_PREFS_DIALOG (object); + g_clear_object (&dialog->plugin_loader); + g_cancellable_cancel (dialog->cancellable); + g_clear_object (&dialog->cancellable); + g_clear_object (&dialog->settings); + + G_OBJECT_CLASS (gs_prefs_dialog_parent_class)->dispose (object); +} + +static void +gs_prefs_dialog_init (GsPrefsDialog *dialog) +{ + gtk_widget_init_template (GTK_WIDGET (dialog)); + + dialog->cancellable = g_cancellable_new (); + dialog->settings = g_settings_new ("org.gnome.software"); + g_settings_bind (dialog->settings, + "download-updates-notify", + dialog->switch_updates_notify, + "active", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (dialog->settings, + "download-updates", + dialog->switch_updates, + "active", + G_SETTINGS_BIND_DEFAULT); +} + +static void +gs_prefs_dialog_class_init (GsPrefsDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_prefs_dialog_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-prefs-dialog.ui"); + gtk_widget_class_bind_template_child (widget_class, GsPrefsDialog, switch_updates); + gtk_widget_class_bind_template_child (widget_class, GsPrefsDialog, switch_updates_notify); +} + +GtkWidget * +gs_prefs_dialog_new (GtkWindow *parent, GsPluginLoader *plugin_loader) +{ + GsPrefsDialog *dialog; + dialog = g_object_new (GS_TYPE_PREFS_DIALOG, + "use-header-bar", TRUE, + "transient-for", parent, + "modal", TRUE, + NULL); + dialog->plugin_loader = g_object_ref (plugin_loader); + return GTK_WIDGET (dialog); +} diff --git a/src/gs-prefs-dialog.h b/src/gs-prefs-dialog.h new file mode 100644 index 0000000..27fb955 --- /dev/null +++ b/src/gs-prefs-dialog.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PREFS_DIALOG (gs_prefs_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPrefsDialog, gs_prefs_dialog, GS, PREFS_DIALOG, GtkDialog) + +GtkWidget *gs_prefs_dialog_new (GtkWindow *parent, + GsPluginLoader *plugin_loader); + +G_END_DECLS diff --git a/src/gs-prefs-dialog.ui b/src/gs-prefs-dialog.ui new file mode 100644 index 0000000..bdf989c --- /dev/null +++ b/src/gs-prefs-dialog.ui @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsPrefsDialog" parent="GtkDialog"> + <property name="title" translatable="yes">Update Preferences</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="resizable">False</property> + <property name="skip_taskbar_hint">True</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <child type="title"> + <object class="GtkLabel" id="label_header"> + <property name="visible">True</property> + <property name="label" translatable="yes">Update Preferences</property> + <property name="selectable">False</property> + <style> + <class name="title"/> + </style> + </object> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="margin_top">24</property> + <property name="margin_bottom">24</property> + <property name="margin_start">33</property> + <property name="margin_end">33</property> + <property name="row_spacing">6</property> + <property name="column_spacing">27</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Automatic Updates</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="switch_updates"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="margin_bottom">15</property> + <property name="label" translatable="yes">Automatic updates are disabled when on mobile or metered connections.</property> + <property name="wrap">True</property> + <property name="width_chars">28</property> + <property name="max_width_chars">28</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Automatic Update Notifications</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Show notifications when updates have been automatically installed.</property> + <property name="wrap">True</property> + <property name="width_chars">28</property> + <property name="max_width_chars">28</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="switch_updates_notify"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-progress-button.c b/src/gs-progress-button.c new file mode 100644 index 0000000..ee6a855 --- /dev/null +++ b/src/gs-progress-button.c @@ -0,0 +1,87 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-app.h" +#include "gs-progress-button.h" + +struct _GsProgressButton +{ + GtkButton parent_instance; + + GtkCssProvider *css_provider; +}; + +G_DEFINE_TYPE (GsProgressButton, gs_progress_button, GTK_TYPE_BUTTON) + +void +gs_progress_button_set_progress (GsProgressButton *button, guint percentage) +{ + g_autofree gchar *css = NULL; + + if (percentage == GS_APP_PROGRESS_UNKNOWN) { + g_warning ("FIXME: Unknown progress handling is not yet implemented for GsProgressButton"); + percentage = 0; + } + + if (percentage == 0) + css = g_strdup (".install-progress { background-size: 0; }"); + else if (percentage == 100) + css = g_strdup (".install-progress { background-size: 100%; }"); + else + css = g_strdup_printf (".install-progress { background-size: %u%%; }", percentage); + + gtk_css_provider_load_from_data (button->css_provider, css, -1, NULL); +} + +void +gs_progress_button_set_show_progress (GsProgressButton *button, gboolean show_progress) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (GTK_WIDGET (button)); + if (show_progress) + gtk_style_context_add_class (context, "install-progress"); + else + gtk_style_context_remove_class (context, "install-progress"); +} + +static void +gs_progress_button_dispose (GObject *object) +{ + GsProgressButton *button = GS_PROGRESS_BUTTON (object); + + g_clear_object (&button->css_provider); + + G_OBJECT_CLASS (gs_progress_button_parent_class)->dispose (object); +} + +static void +gs_progress_button_init (GsProgressButton *button) +{ + button->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (button)), + GTK_STYLE_PROVIDER (button->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +gs_progress_button_class_init (GsProgressButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gs_progress_button_dispose; +} + +GtkWidget * +gs_progress_button_new (void) +{ + return g_object_new (GS_TYPE_PROGRESS_BUTTON, NULL); +} diff --git a/src/gs-progress-button.h b/src/gs-progress-button.h new file mode 100644 index 0000000..e45f0d4 --- /dev/null +++ b/src/gs-progress-button.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_PROGRESS_BUTTON (gs_progress_button_get_type ()) + +G_DECLARE_FINAL_TYPE (GsProgressButton, gs_progress_button, GS, PROGRESS_BUTTON, GtkButton) + +GtkWidget *gs_progress_button_new (void); +void gs_progress_button_set_progress (GsProgressButton *button, + guint percentage); +void gs_progress_button_set_show_progress (GsProgressButton *button, + gboolean show_progress); + +G_END_DECLS diff --git a/src/gs-removal-dialog.c b/src/gs-removal-dialog.c new file mode 100644 index 0000000..61b5cbd --- /dev/null +++ b/src/gs-removal-dialog.c @@ -0,0 +1,172 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-removal-dialog.h" +#include "gs-utils.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +struct _GsRemovalDialog +{ + GtkMessageDialog parent_instance; + GtkWidget *listbox; + GtkWidget *scrolledwindow; +}; + +G_DEFINE_TYPE (GsRemovalDialog, gs_removal_dialog, GTK_TYPE_MESSAGE_DIALOG) + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header = NULL; + if (before != NULL) + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static gint +list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GObject *o1 = G_OBJECT (gtk_bin_get_child (GTK_BIN (a))); + GObject *o2 = G_OBJECT (gtk_bin_get_child (GTK_BIN (b))); + const gchar *key1 = g_object_get_data (o1, "sort"); + const gchar *key2 = g_object_get_data (o2, "sort"); + return g_strcmp0 (key1, key2); +} + +static void +add_app (GtkListBox *listbox, GsApp *app) +{ + GtkWidget *box; + GtkWidget *widget; + GtkWidget *row; + g_autofree gchar *sort_key = NULL; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_set_margin_top (box, 12); + gtk_widget_set_margin_start (box, 12); + gtk_widget_set_margin_bottom (box, 12); + gtk_widget_set_margin_end (box, 12); + + widget = gtk_label_new (gs_app_get_name (app)); + gtk_widget_set_halign (widget, GTK_ALIGN_START); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_container_add (GTK_CONTAINER (box), widget); + + if (gs_app_get_name (app) != NULL) { + sort_key = gs_utils_sort_key (gs_app_get_name (app)); + } + + g_object_set_data_full (G_OBJECT (box), + "sort", + g_steal_pointer (&sort_key), + g_free); + + gtk_list_box_prepend (listbox, box); + gtk_widget_show (widget); + gtk_widget_show (box); + + row = gtk_widget_get_parent (box); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE); +} + +static void +insert_details_widget (GtkMessageDialog *dialog, GtkWidget *widget) +{ + GList *children, *l; + GtkWidget *message_area; + + message_area = gtk_message_dialog_get_message_area (dialog); + g_assert (GTK_IS_BOX (message_area)); + + /* find all label children and set the width chars properties */ + children = gtk_container_get_children (GTK_CONTAINER (message_area)); + for (l = children; l != NULL; l = l->next) { + if (!GTK_IS_LABEL (l->data)) + continue; + + gtk_label_set_width_chars (GTK_LABEL (l->data), 40); + gtk_label_set_max_width_chars (GTK_LABEL (l->data), 40); + } + + gtk_container_add (GTK_CONTAINER (message_area), widget); +} + +void +gs_removal_dialog_show_upgrade_removals (GsRemovalDialog *self, + GsApp *upgrade) +{ + GsAppList *removals; + g_autofree gchar *name_version = NULL; + + name_version = g_strdup_printf ("%s %s", + gs_app_get_name (upgrade), + gs_app_get_version (upgrade)); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (self), + /* TRANSLATORS: This is a text displayed during a distro upgrade. %s + will be replaced by the name and version of distro, e.g. 'Fedora 23'. */ + _("Some of the currently installed software is not compatible with %s. " + "If you continue, the following will be automatically removed during the upgrade:"), + name_version); + + removals = gs_app_get_related (upgrade); + for (guint i = 0; i < gs_app_list_length (removals); i++) { + GsApp *app = gs_app_list_index (removals, i); + g_autofree gchar *tmp = NULL; + + if (gs_app_get_state (app) != AS_APP_STATE_UNAVAILABLE) + continue; + tmp = gs_app_to_string (app); + g_debug ("removal %u: %s", i, tmp); + add_app (GTK_LIST_BOX (self->listbox), app); + } +} + +static void +gs_removal_dialog_init (GsRemovalDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + insert_details_widget (GTK_MESSAGE_DIALOG (self), self->scrolledwindow); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->listbox), + list_header_func, + self, + NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->listbox), + list_sort_func, + self, NULL); +} + +static void +gs_removal_dialog_class_init (GsRemovalDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-removal-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsRemovalDialog, listbox); + gtk_widget_class_bind_template_child (widget_class, GsRemovalDialog, scrolledwindow); +} + +GtkWidget * +gs_removal_dialog_new (void) +{ + GsRemovalDialog *dialog; + + dialog = g_object_new (GS_TYPE_REMOVAL_DIALOG, + NULL); + return GTK_WIDGET (dialog); +} diff --git a/src/gs-removal-dialog.h b/src/gs-removal-dialog.h new file mode 100644 index 0000000..6006afb --- /dev/null +++ b/src/gs-removal-dialog.h @@ -0,0 +1,25 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_REMOVAL_DIALOG (gs_removal_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsRemovalDialog, gs_removal_dialog, GS, REMOVAL_DIALOG, GtkMessageDialog) + +GtkWidget *gs_removal_dialog_new (void); +void gs_removal_dialog_show_upgrade_removals (GsRemovalDialog *self, + GsApp *upgrade); + +G_END_DECLS diff --git a/src/gs-removal-dialog.ui b/src/gs-removal-dialog.ui new file mode 100644 index 0000000..3d03c75 --- /dev/null +++ b/src/gs-removal-dialog.ui @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="min_content_height">160</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkFrame" id="frame"> + <property name="visible">True</property> + <property name="shadow_type">in</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <child> + <object class="GtkListBox" id="listbox"> + <property name="visible">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + <template class="GsRemovalDialog" parent="GtkMessageDialog"> + <property name="text" translatable="yes">Incompatible Software</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <child type="action"> + <object class="GtkButton" id="button_cancel"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Cancel</property> + <property name="use_underline">True</property> + </object> + </child> + <child type="action"> + <object class="GtkButton" id="button_continue"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Continue</property> + <property name="use_underline">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + </object> + </child> + <action-widgets> + <action-widget response="accept" default="true">button_continue</action-widget> + <action-widget response="cancel">button_cancel</action-widget> + </action-widgets> + </template> +</interface> diff --git a/src/gs-repo-row.c b/src/gs-repo-row.c new file mode 100644 index 0000000..72f0416 --- /dev/null +++ b/src/gs-repo-row.c @@ -0,0 +1,290 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-repo-row.h" + +#include <glib/gi18n.h> + +typedef struct +{ + GsApp *repo; + GtkWidget *button; + GtkWidget *name_label; + GtkWidget *comment_label; + GtkWidget *details_revealer; + GtkWidget *status_label; + GtkWidget *url_box; + GtkWidget *url_label; + guint refresh_idle_id; +} GsRepoRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsRepoRow, gs_repo_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + SIGNAL_BUTTON_CLICKED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +void +gs_repo_row_set_name (GsRepoRow *row, const gchar *name) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_label_set_text (GTK_LABEL (priv->name_label), name); + gtk_widget_set_visible (priv->name_label, name != NULL); +} + +void +gs_repo_row_set_comment (GsRepoRow *row, const gchar *comment) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_label_set_markup (GTK_LABEL (priv->comment_label), comment); + gtk_widget_set_visible (priv->comment_label, comment != NULL); +} + +void +gs_repo_row_set_url (GsRepoRow *row, const gchar *url) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_label_set_text (GTK_LABEL (priv->url_label), url); + gtk_widget_set_visible (priv->url_box, url != NULL); +} + +static gboolean +repo_supports_removal (GsApp *repo) +{ + const gchar *management_plugin = gs_app_get_management_plugin (repo); + + /* can't remove a repo, only enable/disable existing ones */ + if (g_strcmp0 (management_plugin, "fwupd") == 0 || + g_strcmp0 (management_plugin, "packagekit") == 0 || + g_strcmp0 (management_plugin, "rpm-ostree") == 0) + return FALSE; + + return TRUE; +} + +static void +refresh_ui (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + if (priv->repo == NULL) { + gtk_widget_set_visible (priv->button, FALSE); + return; + } + + gtk_widget_set_visible (priv->button, TRUE); + + /* set button text */ + switch (gs_app_get_state (priv->repo)) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + /* TRANSLATORS: this is a button in the software repositories + dialog for enabling a repo */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Enable")); + /* enable button */ + gtk_widget_set_sensitive (priv->button, TRUE); + break; + case AS_APP_STATE_INSTALLED: + if (repo_supports_removal (priv->repo)) { + /* TRANSLATORS: this is a button in the software repositories dialog + for removing a repo. The ellipsis indicates that further + steps are required */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Remove…")); + } else { + /* TRANSLATORS: this is a button in the software repositories dialog + for disabling a repo. The ellipsis indicates that further + steps are required */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Disable…")); + } + /* enable button */ + gtk_widget_set_sensitive (priv->button, TRUE); + break; + case AS_APP_STATE_INSTALLING: + /* TRANSLATORS: this is a button in the software repositories dialog + that shows the status of a repo being enabled */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Enabling")); + /* disable button */ + gtk_widget_set_sensitive (priv->button, FALSE); + break; + case AS_APP_STATE_REMOVING: + if (repo_supports_removal (priv->repo)) { + /* TRANSLATORS: this is a button in the software repositories dialog + that shows the status of a repo being removed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Removing")); + } else { + /* TRANSLATORS: this is a button in the software repositories dialog + that shows the status of a repo being disabled */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Disabling")); + } + /* disable button */ + gtk_widget_set_sensitive (priv->button, FALSE); + break; + default: + break; + } + + /* set enabled/disabled label */ + switch (gs_app_get_state (priv->repo)) { + case AS_APP_STATE_INSTALLED: + /* TRANSLATORS: this is a label in the software repositories + dialog that indicates that a repo is enabled. */ + gtk_label_set_text (GTK_LABEL (priv->status_label), _("Enabled")); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + /* TRANSLATORS: this is a label in the software repositories + dialog that indicates that a repo is disabled. */ + gtk_label_set_text (GTK_LABEL (priv->status_label), _("Disabled")); + break; + default: + break; + } +} + +static gboolean +refresh_idle (gpointer user_data) +{ + g_autoptr(GsRepoRow) row = (GsRepoRow *) user_data; + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + refresh_ui (row); + + priv->refresh_idle_id = 0; + return G_SOURCE_REMOVE; +} + +static void +repo_state_changed_cb (GsApp *repo, GParamSpec *pspec, GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + if (priv->refresh_idle_id > 0) + return; + priv->refresh_idle_id = g_idle_add (refresh_idle, g_object_ref (row)); +} + +void +gs_repo_row_set_repo (GsRepoRow *row, GsApp *repo) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + g_assert (priv->repo == NULL); + + priv->repo = g_object_ref (repo); + g_signal_connect_object (priv->repo, "notify::state", + G_CALLBACK (repo_state_changed_cb), + row, 0); + refresh_ui (row); +} + +GsApp * +gs_repo_row_get_repo (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + return priv->repo; +} + +void +gs_repo_row_show_details (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE); + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->details_revealer), TRUE); +} + +void +gs_repo_row_hide_details (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE); + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->details_revealer), FALSE); +} + +void +gs_repo_row_show_status (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + gtk_widget_set_visible (priv->status_label, TRUE); +} + +static void +button_clicked_cb (GtkWidget *widget, GsRepoRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0); +} + +static void +gs_repo_row_destroy (GtkWidget *object) +{ + GsRepoRow *row = GS_REPO_ROW (object); + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + if (priv->repo != NULL) { + g_signal_handlers_disconnect_by_func (priv->repo, repo_state_changed_cb, row); + g_clear_object (&priv->repo); + } + + if (priv->refresh_idle_id != 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + GTK_WIDGET_CLASS (gs_repo_row_parent_class)->destroy (object); +} + +static void +gs_repo_row_init (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + gtk_widget_init_template (GTK_WIDGET (row)); + g_signal_connect (priv->button, "clicked", + G_CALLBACK (button_clicked_cb), row); +} + +static void +gs_repo_row_class_init (GsRepoRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + widget_class->destroy = gs_repo_row_destroy; + + signals [SIGNAL_BUTTON_CLICKED] = + g_signal_new ("button-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsRepoRowClass, button_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-repo-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, button); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, name_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, comment_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, details_revealer); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, status_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, url_box); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, url_label); +} + +GtkWidget * +gs_repo_row_new (void) +{ + return g_object_new (GS_TYPE_REPO_ROW, NULL); +} diff --git a/src/gs-repo-row.h b/src/gs-repo-row.h new file mode 100644 index 0000000..ff2564e --- /dev/null +++ b/src/gs-repo-row.h @@ -0,0 +1,40 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gnome-software-private.h" +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_REPO_ROW (gs_repo_row_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsRepoRow, gs_repo_row, GS, REPO_ROW, GtkListBoxRow) + +struct _GsRepoRowClass +{ + GtkListBoxRowClass parent_class; + void (*button_clicked) (GsRepoRow *row); +}; + +GtkWidget *gs_repo_row_new (void); +void gs_repo_row_set_name (GsRepoRow *row, + const gchar *name); +void gs_repo_row_set_comment (GsRepoRow *row, + const gchar *comment); +void gs_repo_row_set_url (GsRepoRow *row, + const gchar *url); +void gs_repo_row_set_repo (GsRepoRow *row, + GsApp *repo); +GsApp *gs_repo_row_get_repo (GsRepoRow *row); +void gs_repo_row_show_details (GsRepoRow *row); +void gs_repo_row_hide_details (GsRepoRow *row); +void gs_repo_row_show_status (GsRepoRow *row); + +G_END_DECLS diff --git a/src/gs-repo-row.ui b/src/gs-repo-row.ui new file mode 100644 index 0000000..2cbe025 --- /dev/null +++ b/src/gs-repo-row.ui @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsRepoRow" parent="GtkListBoxRow"> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="orientation">horizontal</property> + <property name="spacing">16</property> + <child> + <object class="GtkBox" id="vbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="hexpand">True</property> + <child> + <object class="GtkBox" id="hbox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">4</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + </object> + </child> + <child> + <object class="GtkLabel" id="status_label"> + <property name="visible">False</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="comment_label"> + <property name="visible">False</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkRevealer" id="details_revealer"> + <property name="visible">True</property> + <property name="transition-type">slide-down</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkBox" id="url_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">8</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="label" translatable="yes">URL</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="url_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="ellipsize">end</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">8</property> + <child> + <object class="GtkButton" id="button"> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="width_request">105</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="valign">start</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-repos-dialog.c b/src/gs-repos-dialog.c new file mode 100644 index 0000000..53bbf2f --- /dev/null +++ b/src/gs-repos-dialog.c @@ -0,0 +1,890 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-repos-dialog.h" + +#include "gnome-software-private.h" +#include "gs-common.h" +#include "gs-os-release.h" +#include "gs-repo-row.h" +#include "gs-third-party-repo-row.h" +#include "gs-utils.h" +#include <glib/gi18n.h> + +struct _GsReposDialog +{ + GtkDialog parent_instance; + GSettings *settings; + GsApp *third_party_repo; + + GCancellable *cancellable; + GsPluginLoader *plugin_loader; + GtkWidget *frame_third_party; + GtkWidget *label_description; + GtkWidget *label_empty; + GtkWidget *label_header; + GtkWidget *listbox; + GtkWidget *listbox_third_party; + GtkWidget *row_third_party; + GtkWidget *spinner; + GtkWidget *stack; +}; + +G_DEFINE_TYPE (GsReposDialog, gs_repos_dialog, GTK_TYPE_DIALOG) + +typedef struct { + GsReposDialog *dialog; + GsApp *repo; + GsPluginAction action; +} InstallRemoveData; + +static void +install_remove_data_free (InstallRemoveData *data) +{ + g_clear_object (&data->dialog); + g_clear_object (&data->repo); + g_slice_free (InstallRemoveData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstallRemoveData, install_remove_data_free); + +static void reload_sources (GsReposDialog *dialog); +static void reload_third_party_repo (GsReposDialog *dialog); + +static gchar * +get_repo_installed_text (GsApp *repo) +{ + GsAppList *related; + guint cnt_addon = 0; + guint cnt_apps = 0; + g_autofree gchar *addons_text = NULL; + g_autofree gchar *apps_text = NULL; + + related = gs_app_get_related (repo); + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_tmp = gs_app_list_index (related, i); + switch (gs_app_get_kind (app_tmp)) { + case AS_APP_KIND_WEB_APP: + case AS_APP_KIND_DESKTOP: + cnt_apps++; + break; + case AS_APP_KIND_FONT: + case AS_APP_KIND_CODEC: + case AS_APP_KIND_INPUT_METHOD: + case AS_APP_KIND_ADDON: + cnt_addon++; + break; + default: + break; + } + } + + if (cnt_apps == 0 && cnt_addon == 0) { + /* nothing! */ + return NULL; + } + if (cnt_addon == 0) { + /* TRANSLATORS: This string is used to construct the 'X applications + installed' sentence, describing a software repository. */ + return g_strdup_printf (ngettext ("%u application installed", + "%u applications installed", + cnt_apps), cnt_apps); + } + if (cnt_apps == 0) { + /* TRANSLATORS: This string is used to construct the 'X add-ons + installed' sentence, describing a software repository. */ + return g_strdup_printf (ngettext ("%u add-on installed", + "%u add-ons installed", + cnt_addon), cnt_addon); + } + + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the number of applications. */ + apps_text = g_strdup_printf (ngettext ("%u application", + "%u applications", + cnt_apps), cnt_apps); + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the number of add-ons. */ + addons_text = g_strdup_printf (ngettext ("%u add-on", + "%u add-ons", + cnt_addon), cnt_addon); + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the total number of + applications and add-ons. */ + return g_strdup_printf (ngettext ("%s and %s installed", + "%s and %s installed", + cnt_apps + cnt_addon), + apps_text, addons_text); +} + +static gboolean +repo_supports_removal (GsApp *repo) +{ + const gchar *management_plugin = gs_app_get_management_plugin (repo); + + /* can't remove a repo, only enable/disable existing ones */ + if (g_strcmp0 (management_plugin, "fwupd") == 0 || + g_strcmp0 (management_plugin, "packagekit") == 0 || + g_strcmp0 (management_plugin, "rpm-ostree") == 0) + return FALSE; + + return TRUE; +} + +static void +repo_enabled_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(InstallRemoveData) install_remove_data = (InstallRemoveData *) user_data; + g_autoptr(GError) error = NULL; + const gchar *action_str; + + action_str = gs_plugin_action_to_string (install_remove_data->action); + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("repo %s cancelled", action_str); + return; + } + + g_warning ("failed to %s repo: %s", action_str, error->message); + return; + } + + g_debug ("finished %s repo %s", action_str, gs_app_get_id (install_remove_data->repo)); +} + +static void +_enable_repo (InstallRemoveData *install_data) +{ + GsReposDialog *dialog = install_data->dialog; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_debug ("enabling repo %s", gs_app_get_id (install_data->repo)); + plugin_job = gs_plugin_job_newv (install_data->action, + "interactive", TRUE, + "app", install_data->repo, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job, + dialog->cancellable, + repo_enabled_cb, + install_data); +} + +static void +enable_repo_response_cb (GtkDialog *confirm_dialog, + gint response, + gpointer user_data) +{ + g_autoptr(InstallRemoveData) install_data = (InstallRemoveData *) user_data; + + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (confirm_dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + _enable_repo (g_steal_pointer (&install_data)); +} + +static void +enable_repo (GsReposDialog *dialog, GsApp *repo) +{ + g_autoptr(InstallRemoveData) install_data = NULL; + + install_data = g_slice_new0 (InstallRemoveData); + install_data->action = GS_PLUGIN_ACTION_INSTALL; + install_data->repo = g_object_ref (repo); + install_data->dialog = g_object_ref (dialog); + + /* user needs to confirm acceptance of an agreement */ + if (gs_app_get_agreement (repo) != NULL) { + GtkWidget *confirm_dialog; + g_autofree gchar *message = NULL; + g_autoptr(GError) error = NULL; + + /* convert from AppStream markup */ + message = as_markup_convert_simple (gs_app_get_agreement (repo), &error); + if (message == NULL) { + /* failed, so just try and show the original markup */ + message = g_strdup (gs_app_get_agreement (repo)); + g_warning ("Failed to process AppStream markup: %s", + error->message); + } + + /* ask for confirmation */ + confirm_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + /* TRANSLATORS: window title */ + "%s", _("Enable Third-Party Software Repository?")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (confirm_dialog), + "%s", message); + + /* TRANSLATORS: button to accept the agreement */ + gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Enable"), + GTK_RESPONSE_OK); + + /* handle this async */ + g_signal_connect (confirm_dialog, "response", + G_CALLBACK (enable_repo_response_cb), + g_steal_pointer (&install_data)); + + gtk_window_set_modal (GTK_WINDOW (confirm_dialog), TRUE); + gtk_window_present (GTK_WINDOW (confirm_dialog)); + return; + } + + /* no prompt required */ + _enable_repo (g_steal_pointer (&install_data)); +} + +static void +remove_repo_response_cb (GtkDialog *confirm_dialog, + gint response, + gpointer user_data) +{ + g_autoptr(InstallRemoveData) remove_data = (InstallRemoveData *) user_data; + GsReposDialog *dialog = remove_data->dialog; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (confirm_dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + g_debug ("removing repo %s", gs_app_get_id (remove_data->repo)); + plugin_job = gs_plugin_job_newv (remove_data->action, + "interactive", TRUE, + "app", remove_data->repo, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job, + dialog->cancellable, + repo_enabled_cb, + g_steal_pointer (&remove_data)); +} + +static void +remove_confirm_repo (GsReposDialog *dialog, GsApp *repo) +{ + InstallRemoveData *remove_data; + GtkWidget *confirm_dialog; + g_autofree gchar *message = NULL; + g_autofree gchar *title = NULL; + GtkWidget *button; + GtkStyleContext *context; + + remove_data = g_slice_new0 (InstallRemoveData); + remove_data->action = GS_PLUGIN_ACTION_REMOVE; + remove_data->repo = g_object_ref (repo); + remove_data->dialog = g_object_ref (dialog); + + if (repo_supports_removal (repo)) { + /* TRANSLATORS: this is a prompt message, and '%s' is a + * repository name, e.g. 'GNOME Nightly' */ + title = g_strdup_printf (_("Remove “%s”?"), + gs_app_get_name (repo)); + } else { + /* TRANSLATORS: this is a prompt message, and '%s' is a + * repository name, e.g. 'GNOME Nightly' */ + title = g_strdup_printf (_("Disable “%s”?"), + gs_app_get_name (repo)); + } + /* TRANSLATORS: longer dialog text */ + message = g_strdup (_("Software that has been installed from this " + "repository will no longer receive updates, " + "including security fixes.")); + + /* ask for confirmation */ + confirm_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + "%s", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (confirm_dialog), + "%s", message); + + if (repo_supports_removal (repo)) { + /* TRANSLATORS: this is button text to remove the repo */ + button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Remove"), GTK_RESPONSE_OK); + } else { + /* TRANSLATORS: this is button text to remove the repo */ + button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Disable"), GTK_RESPONSE_OK); + } + + context = gtk_widget_get_style_context (button); + gtk_style_context_add_class (context, "destructive-action"); + + /* handle this async */ + g_signal_connect (confirm_dialog, "response", + G_CALLBACK (remove_repo_response_cb), remove_data); + + gtk_window_set_modal (GTK_WINDOW (confirm_dialog), TRUE); + gtk_window_present (GTK_WINDOW (confirm_dialog)); +} + +static void +repo_button_clicked_cb (GsRepoRow *row, + GsReposDialog *dialog) +{ + GsApp *repo; + + repo = gs_repo_row_get_repo (row); + + switch (gs_app_get_state (repo)) { + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + enable_repo (dialog, repo); + break; + case AS_APP_STATE_INSTALLED: + remove_confirm_repo (dialog, repo); + break; + default: + g_warning ("repo %s button clicked in unexpected state %s", + gs_app_get_id (repo), + as_app_state_to_string (gs_app_get_state (repo))); + break; + } +} + +static GtkListBox * +get_list_box_for_repo (GsReposDialog *dialog, GsApp *repo) +{ + if (dialog->third_party_repo != NULL) { + const gchar *source_repo; + const gchar *source_third_party_package; + + source_repo = gs_app_get_source_id_default (repo); + source_third_party_package = gs_app_get_source_id_default (dialog->third_party_repo); + + /* group repos from the same repo-release package together */ + if (g_strcmp0 (source_repo, source_third_party_package) == 0) + return GTK_LIST_BOX (dialog->listbox_third_party); + } + + return GTK_LIST_BOX (dialog->listbox); +} + +static void +add_repo (GsReposDialog *dialog, GsApp *repo) +{ + GtkWidget *row; + g_autofree gchar *text = NULL; + AsAppState state; + + state = gs_app_get_state (repo); + if (!(state == AS_APP_STATE_AVAILABLE || + state == AS_APP_STATE_AVAILABLE_LOCAL || + state == AS_APP_STATE_INSTALLED || + state == AS_APP_STATE_INSTALLING || + state == AS_APP_STATE_REMOVING)) { + g_warning ("repo %s in invalid state %s", + gs_app_get_id (repo), + as_app_state_to_string (state)); + return; + } + + row = gs_repo_row_new (); + gs_repo_row_set_name (GS_REPO_ROW (row), + gs_app_get_name (repo)); + text = get_repo_installed_text (repo); + gs_repo_row_set_comment (GS_REPO_ROW (row), text); + gs_repo_row_set_url (GS_REPO_ROW (row), + gs_app_get_url (repo, AS_URL_KIND_HOMEPAGE)); + gs_repo_row_show_status (GS_REPO_ROW (row)); + gs_repo_row_set_repo (GS_REPO_ROW (row), repo); + + g_signal_connect (row, "button-clicked", + G_CALLBACK (repo_button_clicked_cb), dialog); + + gtk_list_box_prepend (get_list_box_for_repo (dialog, repo), row); + gtk_widget_show (row); +} + +static void +third_party_repo_installed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(InstallRemoveData) install_data = (InstallRemoveData *) user_data; + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + const gchar *action_str = gs_plugin_action_to_string (install_data->action); + + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("third party repo %s cancelled", action_str); + return; + } + + g_warning ("failed to %s third party repo: %s", action_str, error->message); + return; + } + + reload_sources (install_data->dialog); +} + +static void +install_third_party_repo (GsReposDialog *dialog, gboolean install) +{ + InstallRemoveData *install_data; + g_autoptr(GsPluginJob) plugin_job = NULL; + + install_data = g_slice_new0 (InstallRemoveData); + install_data->dialog = g_object_ref (dialog); + install_data->action = install ? GS_PLUGIN_ACTION_INSTALL : GS_PLUGIN_ACTION_REMOVE; + + plugin_job = gs_plugin_job_newv (install_data->action, + "interactive", TRUE, + "app", dialog->third_party_repo, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, + plugin_job, + dialog->cancellable, + third_party_repo_installed_cb, + install_data); +} + +static void +third_party_repo_button_clicked_cb (GsThirdPartyRepoRow *row, + gpointer user_data) +{ + GsReposDialog *dialog = (GsReposDialog *) user_data; + GsApp *app; + + app = gs_third_party_repo_row_get_app (row); + + switch (gs_app_get_state (app)) { + case AS_APP_STATE_UNAVAILABLE: + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + install_third_party_repo (dialog, TRUE); + break; + case AS_APP_STATE_UPDATABLE_LIVE: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_INSTALLED: + install_third_party_repo (dialog, FALSE); + break; + default: + g_warning ("third party repo %s button clicked in unexpected state %s", + gs_app_get_id (app), + as_app_state_to_string (gs_app_get_state (app))); + break; + } + + g_settings_set_boolean (dialog->settings, "show-nonfree-prompt", FALSE); +} + +static void +refresh_third_party_repo (GsReposDialog *dialog) +{ + if (dialog->third_party_repo == NULL) { + gtk_widget_hide (dialog->frame_third_party); + return; + } + + gtk_widget_show (dialog->frame_third_party); +} + +static void +remove_all_repo_rows_cb (GtkWidget *widget, gpointer user_data) +{ + GtkContainer *container = GTK_CONTAINER (user_data); + + if (GS_IS_REPO_ROW (widget)) + gtk_container_remove (container, widget); +} + +static void +container_remove_all_repo_rows (GtkContainer *container) +{ + gtk_container_foreach (container, remove_all_repo_rows_cb, container); +} + +static void +get_sources_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsReposDialog *dialog) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("get sources cancelled"); + return; + } else { + g_warning ("failed to get sources: %s", error->message); + } + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "empty"); + gtk_style_context_add_class (gtk_widget_get_style_context (dialog->label_header), + "dim-label"); + return; + } + + /* remove previous */ + gs_container_remove_all (GTK_CONTAINER (dialog->listbox)); + container_remove_all_repo_rows (GTK_CONTAINER (dialog->listbox_third_party)); + + /* stop the spinner */ + gs_stop_spinner (GTK_SPINNER (dialog->spinner)); + + /* no results */ + if (gs_app_list_length (list) == 0) { + g_debug ("no sources to show"); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "empty"); + gtk_style_context_add_class (gtk_widget_get_style_context (dialog->label_header), "dim-label"); + return; + } + + gtk_style_context_remove_class (gtk_widget_get_style_context (dialog->label_header), + "dim-label"); + + /* add each */ + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "sources"); + for (guint i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + add_repo (dialog, app); + } +} + +static void +resolve_third_party_repo_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsReposDialog *dialog) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("resolve third party repo cancelled"); + return; + } else { + g_warning ("failed to resolve third party repo: %s", error->message); + return; + } + } + + /* we should only get one result */ + if (gs_app_list_length (list) > 0) + app = gs_app_list_index (list, 0); + else + app = NULL; + + g_set_object (&dialog->third_party_repo, app); + gs_third_party_repo_row_set_app (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party), app); + + /* refresh widget */ + refresh_third_party_repo (dialog); +} + +static void +reload_sources (GsReposDialog *dialog) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the list of non-core software repositories */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job, + dialog->cancellable, + (GAsyncReadyCallback) get_sources_cb, + dialog); +} + +static gboolean +is_fedora (void) +{ + const gchar *id = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + os_release = gs_os_release_new (NULL); + if (os_release == NULL) + return FALSE; + + id = gs_os_release_get_id (os_release); + if (g_strcmp0 (id, "fedora") == 0) + return TRUE; + + return FALSE; +} + +static void +reload_third_party_repo (GsReposDialog *dialog) +{ + const gchar *third_party_repo_package = "fedora-workstation-repositories"; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* Fedora-specific functionality */ + if (!is_fedora ()) + return; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES, + "search", third_party_repo_package, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job, + dialog->cancellable, + (GAsyncReadyCallback) resolve_third_party_repo_cb, + dialog); +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header = NULL; + if (before != NULL) + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static gchar * +get_row_sort_key (GtkListBoxRow *row) +{ + GsApp *app; + guint sort_order; + g_autofree gchar *sort_key = NULL; + + /* sort third party repo rows first */ + if (GS_IS_THIRD_PARTY_REPO_ROW (row)) { + sort_order = 1; + app = gs_third_party_repo_row_get_app (GS_THIRD_PARTY_REPO_ROW (row)); + } else { + sort_order = 2; + app = gs_repo_row_get_repo (GS_REPO_ROW (row)); + } + + if (gs_app_get_name (app) != NULL) { + sort_key = gs_utils_sort_key (gs_app_get_name (app)); + return g_strdup_printf ("%u:%s", sort_order, sort_key); + } else { + return g_strdup_printf ("%u:", sort_order); + } +} + +static gint +list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + g_autofree gchar *key1 = get_row_sort_key (a); + g_autofree gchar *key2 = get_row_sort_key (b); + + /* compare the keys according to the algorithm above */ + return g_strcmp0 (key1, key2); +} + +static void +list_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsReposDialog *dialog) +{ + GtkListBoxRow *other_row; + + if (!GS_IS_REPO_ROW (row)) + return; + + gs_repo_row_show_details (GS_REPO_ROW (row)); + + for (guint i = 0; (other_row = gtk_list_box_get_row_at_index (list_box, i)) != NULL; i++) { + if (!GS_IS_REPO_ROW (other_row)) + continue; + if (other_row == row) + continue; + + gs_repo_row_hide_details (GS_REPO_ROW (other_row)); + } +} + +static gchar * +get_os_name (void) +{ + gchar *name = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + os_release = gs_os_release_new (NULL); + if (os_release != NULL) + name = g_strdup (gs_os_release_get_name (os_release)); + if (name == NULL) { + /* TRANSLATORS: this is the fallback text we use if we can't + figure out the name of the operating system */ + name = g_strdup (_("the operating system")); + } + + return name; +} + +static void +reload_cb (GsPluginLoader *plugin_loader, GsReposDialog *dialog) +{ + reload_sources (dialog); + reload_third_party_repo (dialog); +} + +static void +set_plugin_loader (GsReposDialog *dialog, GsPluginLoader *plugin_loader) +{ + dialog->plugin_loader = g_object_ref (plugin_loader); + g_signal_connect (dialog->plugin_loader, "reload", + G_CALLBACK (reload_cb), dialog); +} + +static void +gs_repos_dialog_dispose (GObject *object) +{ + GsReposDialog *dialog = GS_REPOS_DIALOG (object); + + if (dialog->plugin_loader != NULL) { + g_signal_handlers_disconnect_by_func (dialog->plugin_loader, reload_cb, dialog); + g_clear_object (&dialog->plugin_loader); + } + + g_cancellable_cancel (dialog->cancellable); + g_clear_object (&dialog->cancellable); + g_clear_object (&dialog->settings); + g_clear_object (&dialog->third_party_repo); + + G_OBJECT_CLASS (gs_repos_dialog_parent_class)->dispose (object); +} + +static void +gs_repos_dialog_init (GsReposDialog *dialog) +{ + g_autofree gchar *label_description_text = NULL; + g_autofree gchar *label_empty_text = NULL; + g_autofree gchar *os_name = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + gtk_widget_init_template (GTK_WIDGET (dialog)); + + dialog->cancellable = g_cancellable_new (); + dialog->settings = g_settings_new ("org.gnome.software"); + + os_name = get_os_name (); + + gtk_list_box_set_header_func (GTK_LIST_BOX (dialog->listbox), + list_header_func, + dialog, + NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (dialog->listbox), + list_sort_func, + dialog, NULL); + g_signal_connect (dialog->listbox, "row-activated", + G_CALLBACK (list_row_activated_cb), dialog); + + /* TRANSLATORS: This is the description text displayed in the Software Repositories dialog. + %s gets replaced by the name of the actual distro, e.g. Fedora. */ + label_description_text = g_strdup_printf (_("These repositories supplement the default software provided by %s."), + os_name); + gtk_label_set_text (GTK_LABEL (dialog->label_description), label_description_text); + + /* set up third party repository row */ + gtk_list_box_set_header_func (GTK_LIST_BOX (dialog->listbox_third_party), + list_header_func, + dialog, + NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (dialog->listbox_third_party), + list_sort_func, + dialog, NULL); + g_signal_connect (dialog->listbox_third_party, "row-activated", + G_CALLBACK (list_row_activated_cb), dialog); + g_signal_connect (dialog->row_third_party, "button-clicked", + G_CALLBACK (third_party_repo_button_clicked_cb), dialog); + gs_third_party_repo_row_set_name (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party), + /* TRANSLATORS: info bar title in the software repositories dialog */ + _("Third Party Repositories")); + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Access additional software from selected third party sources.")); + g_string_append (str, " "); + g_string_append (str, + /* TRANSLATORS: this is the third party repositories info bar. */ + _("Some of this software is proprietary and therefore has restrictions on use, sharing, and access to source code.")); + g_string_append_printf (str, " <a href=\"%s\">%s</a>", + "https://fedoraproject.org/wiki/Workstation/Third_Party_Software_Repositories", + /* TRANSLATORS: this is the clickable + * link on the third party repositories info bar */ + _("Find out more…")); + gs_third_party_repo_row_set_comment (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party), str->str); + refresh_third_party_repo (dialog); + + /* TRANSLATORS: This is the description text displayed in the Software Repositories dialog. + %s gets replaced by the name of the actual distro, e.g. Fedora. */ + label_empty_text = g_strdup_printf (_("These repositories supplement the default software provided by %s."), + os_name); + gtk_label_set_text (GTK_LABEL (dialog->label_empty), label_empty_text); +} + +static void +gs_repos_dialog_class_init (GsReposDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_repos_dialog_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-repos-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, frame_third_party); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_description); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_empty); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_header); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, listbox); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, listbox_third_party); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, row_third_party); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, spinner); + gtk_widget_class_bind_template_child (widget_class, GsReposDialog, stack); +} + +GtkWidget * +gs_repos_dialog_new (GtkWindow *parent, GsPluginLoader *plugin_loader) +{ + GsReposDialog *dialog; + + dialog = g_object_new (GS_TYPE_REPOS_DIALOG, + "use-header-bar", TRUE, + "transient-for", parent, + "modal", TRUE, + NULL); + set_plugin_loader (dialog, plugin_loader); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "waiting"); + gs_start_spinner (GTK_SPINNER (dialog->spinner)); + reload_sources (dialog); + reload_third_party_repo (dialog); + + return GTK_WIDGET (dialog); +} diff --git a/src/gs-repos-dialog.h b/src/gs-repos-dialog.h new file mode 100644 index 0000000..ff82350 --- /dev/null +++ b/src/gs-repos-dialog.h @@ -0,0 +1,25 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_REPOS_DIALOG (gs_repos_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsReposDialog, gs_repos_dialog, GS, REPOS_DIALOG, GtkDialog) + +GtkWidget *gs_repos_dialog_new (GtkWindow *parent, + GsPluginLoader *plugin_loader); + +G_END_DECLS diff --git a/src/gs-repos-dialog.ui b/src/gs-repos-dialog.ui new file mode 100644 index 0000000..b53b574 --- /dev/null +++ b/src/gs-repos-dialog.ui @@ -0,0 +1,171 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsReposDialog" parent="GtkDialog"> + <property name="title" translatable="yes">Software Repositories</property> + <property name="modal">True</property> + <property name="default_width">600</property> + <property name="default_height">600</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <child type="title"> + <object class="GtkLabel" id="label_header"> + <property name="visible">True</property> + <property name="label" translatable="yes">Software Repositories</property> + <property name="selectable">False</property> + <style> + <class name="title"/> + </style> + </object> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkSpinner" id="spinner"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + <packing> + <property name="name">waiting</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_empty"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="spacing">16</property> + <property name="orientation">vertical</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="icon_empty"> + <property name="visible">True</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + <property name="pixel-size">64</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_empty_title"> + <property name="visible">True</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="label" translatable="yes">No Additional Repositories</property> + <property name="max_width_chars">40</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_empty"> + <property name="visible">True</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max_width_chars">40</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + </child> + </object> + <packing> + <property name="name">empty</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="margin_start">52</property> + <property name="margin_end">52</property> + <property name="margin_top">24</property> + <property name="margin_bottom">40</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + <child> + <object class="GtkLabel" id="label_description"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + <property name="max_width_chars">30</property> + <property name="margin_bottom">16</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkFrame" id="frame_third_party"> + <property name="visible">True</property> + <property name="shadow_type">in</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="margin_bottom">16</property> + <child> + <object class="GtkListBox" id="listbox_third_party"> + <property name="visible">True</property> + <property name="selection_mode">none</property> + <child> + <object class="GsThirdPartyRepoRow" id="row_third_party"> + <property name="visible">True</property> + <property name="activatable">False</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame" id="frame"> + <property name="visible">True</property> + <property name="shadow_type">in</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="vexpand">True</property> + <property name="margin_top">9</property> + <child> + <object class="GtkListBox" id="listbox"> + <property name="visible">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">sources</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-restarter.c b/src/gs-restarter.c new file mode 100644 index 0000000..f4c0b47 --- /dev/null +++ b/src/gs-restarter.c @@ -0,0 +1,216 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gio/gio.h> +#include <stdlib.h> + +#define GS_BINARY_NAME "gnome-software" +#define GS_DBUS_BUS_NAME "org.gnome.Software" +#define GS_DBUS_OBJECT_PATH "/org/gnome/Software" +#define GS_DBUS_INTERFACE_NAME "org.gtk.Actions" + +typedef struct { + GMainLoop *loop; + GDBusConnection *connection; + gboolean is_running; + gboolean timed_out; +} GsRestarterPrivate; + +static void +gs_restarter_on_name_appeared_cb (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GsRestarterPrivate *priv = (GsRestarterPrivate *) user_data; + priv->is_running = TRUE; + g_debug ("%s appeared", GS_DBUS_BUS_NAME); + if (g_main_loop_is_running (priv->loop)) + g_main_loop_quit (priv->loop); +} + +static void +gs_restarter_on_name_vanished_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GsRestarterPrivate *priv = (GsRestarterPrivate *) user_data; + priv->is_running = FALSE; + g_debug ("%s vanished", GS_DBUS_BUS_NAME); + if (g_main_loop_is_running (priv->loop)) + g_main_loop_quit (priv->loop); +} + +static gboolean +gs_restarter_loop_timeout_cb (gpointer user_data) +{ + GsRestarterPrivate *priv = (GsRestarterPrivate *) user_data; + priv->timed_out = TRUE; + g_main_loop_quit (priv->loop); + return TRUE; +} + +static gboolean +gs_restarter_wait_for_timeout (GsRestarterPrivate *priv, + guint timeout_ms, + GError **error) +{ + guint timer_id; + priv->timed_out = FALSE; + timer_id = g_timeout_add (timeout_ms, gs_restarter_loop_timeout_cb, priv); + g_main_loop_run (priv->loop); + g_source_remove (timer_id); + if (priv->timed_out) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_TIMED_OUT, + "Waited for %ums", timeout_ms); + return FALSE; + } + return TRUE; +} + +static GsRestarterPrivate * +gs_restarter_private_new (void) +{ + GsRestarterPrivate *priv = g_new0 (GsRestarterPrivate, 1); + priv->loop = g_main_loop_new (NULL, FALSE); + return priv; +} + +static void +gs_restarter_private_free (GsRestarterPrivate *priv) +{ + if (priv->connection != NULL) + g_object_unref (priv->connection); + g_main_loop_unref (priv->loop); + g_free (priv); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsRestarterPrivate, gs_restarter_private_free) + +static gboolean +gs_restarter_create_new_process (GsRestarterPrivate *priv, GError **error) +{ + g_autofree gchar *binary_filename = NULL; + + /* start executable */ + binary_filename = g_build_filename (BINDIR, GS_BINARY_NAME, NULL); + g_debug ("starting new binary %s", binary_filename); + if (!g_spawn_command_line_async (binary_filename, error)) + return FALSE; + + /* wait for the bus name to appear */ + if (!gs_restarter_wait_for_timeout (priv, 15000, error)) { + g_prefix_error (error, "%s did not appear: ", GS_DBUS_BUS_NAME); + return FALSE; + } + + return TRUE; +} + +static gboolean +gs_restarter_destroy_old_process (GsRestarterPrivate *priv, GError **error) +{ + GVariantBuilder args_params; + GVariantBuilder args_platform_data; + g_autoptr(GVariant) reply = NULL; + + /* call a GtkAction */ + g_variant_builder_init (&args_params, g_variant_type_new ("av")); + g_variant_builder_init (&args_platform_data, g_variant_type_new ("a{sv}")); + reply = g_dbus_connection_call_sync (priv->connection, + GS_DBUS_BUS_NAME, + GS_DBUS_OBJECT_PATH, + GS_DBUS_INTERFACE_NAME, + "Activate", + g_variant_new ("(sava{sv})", + "shutdown", + &args_params, + &args_platform_data), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 5000, + NULL, + error); + if (reply == NULL) { + g_prefix_error (error, "Failed to send RequestShutdown: "); + return FALSE; + } + + /* wait for the name to disappear from the bus */ + if (!gs_restarter_wait_for_timeout (priv, 30000, error)) { + g_prefix_error (error, "Failed to see %s vanish: ", GS_DBUS_BUS_NAME); + return FALSE; + } + + return TRUE; +} + +static gboolean +gs_restarter_setup_watcher (GsRestarterPrivate *priv, GError **error) +{ + /* watch the name appear and vanish */ + priv->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error); + if (priv->connection == NULL) { + g_prefix_error (error, "Failed to get D-Bus connection: "); + return FALSE; + } + g_bus_watch_name_on_connection (priv->connection, + GS_DBUS_BUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + gs_restarter_on_name_appeared_cb, + gs_restarter_on_name_vanished_cb, + priv, + NULL); + + /* wait for one of the callbacks to be called */ + if (!gs_restarter_wait_for_timeout (priv, 50, error)) { + g_prefix_error (error, "Failed to watch %s: ", GS_DBUS_BUS_NAME); + return FALSE; + } + + return TRUE; +} + +int +main (int argc, char **argv) +{ + g_autoptr(GsRestarterPrivate) priv = NULL; + g_autoptr(GError) error = NULL; + + /* show all debugging */ + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* set up the watcher */ + priv = gs_restarter_private_new (); + if (!gs_restarter_setup_watcher (priv, &error)) { + g_warning ("Failed to set up: %s", error->message); + return EXIT_FAILURE; + } + + /* kill the old process */ + if (priv->is_running) { + if (!gs_restarter_destroy_old_process (priv, &error)) { + g_warning ("Failed to quit service: %s", error->message); + return EXIT_FAILURE; + } + } + + /* start a new process */ + if (!gs_restarter_create_new_process (priv, &error)) { + g_warning ("Failed to start service: %s", error->message); + return EXIT_FAILURE; + } + + /* success */ + g_debug ("%s process successfully restarted", GS_DBUS_BUS_NAME); + return EXIT_SUCCESS; +} diff --git a/src/gs-review-bar.c b/src/gs-review-bar.c new file mode 100644 index 0000000..4f58c83 --- /dev/null +++ b/src/gs-review-bar.c @@ -0,0 +1,77 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <math.h> + +#include "gs-review-bar.h" + +struct _GsReviewBar +{ + GtkBin parent_instance; + gdouble fraction; +}; + +G_DEFINE_TYPE (GsReviewBar, gs_review_bar, GTK_TYPE_BIN) + +void +gs_review_bar_set_fraction (GsReviewBar *bar, gdouble fraction) +{ + g_return_if_fail (GS_IS_REVIEW_BAR (bar)); + bar->fraction = fraction; +} + +static void +gs_review_bar_init (GsReviewBar *bar) +{ +} + +static gboolean +gs_review_bar_draw (GtkWidget *widget, cairo_t *cr) +{ + GtkStyleContext *context; + gdouble y_offset, bar_height; + GdkRGBA color; + + context = gtk_widget_get_style_context (widget); + + /* don't fill the complete height (too heavy beside GtkLabel of that height) */ + y_offset = floor (0.15 * gtk_widget_get_allocated_height (widget)); + bar_height = gtk_widget_get_allocated_height (widget) - (y_offset * 2); + + gtk_render_background (context, cr, + 0, y_offset, + gtk_widget_get_allocated_width (widget), + bar_height); + + cairo_rectangle (cr, + 0, y_offset, + round (GS_REVIEW_BAR (widget)->fraction * gtk_widget_get_allocated_width (widget)), + bar_height); + gtk_style_context_get_color (context, gtk_widget_get_state_flags (widget), &color); + cairo_set_source_rgba (cr, color.red, color.green, color.blue, color.alpha); + cairo_fill (cr); + + return GTK_WIDGET_CLASS (gs_review_bar_parent_class)->draw (widget, cr); +} + +static void +gs_review_bar_class_init (GsReviewBarClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + widget_class->draw = gs_review_bar_draw; +} + +GtkWidget * +gs_review_bar_new (void) +{ + GsReviewBar *bar; + bar = g_object_new (GS_TYPE_REVIEW_BAR, NULL); + return GTK_WIDGET (bar); +} diff --git a/src/gs-review-bar.h b/src/gs-review-bar.h new file mode 100644 index 0000000..e09dca0 --- /dev/null +++ b/src/gs-review-bar.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_REVIEW_BAR (gs_review_bar_get_type ()) + +G_DECLARE_FINAL_TYPE (GsReviewBar, gs_review_bar, GS, REVIEW_BAR, GtkBin) + +GtkWidget *gs_review_bar_new (void); + +void gs_review_bar_set_fraction (GsReviewBar *bar, + gdouble fraction); + +G_END_DECLS diff --git a/src/gs-review-dialog.c b/src/gs-review-dialog.c new file mode 100644 index 0000000..686209e --- /dev/null +++ b/src/gs-review-dialog.c @@ -0,0 +1,231 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#ifdef HAVE_GSPELL +#include <gspell/gspell.h> +#endif + +#include "gs-review-dialog.h" +#include "gs-star-widget.h" + +#define DESCRIPTION_LENGTH_MAX 3000 /* chars */ +#define DESCRIPTION_LENGTH_MIN 15 /* chars */ +#define SUMMARY_LENGTH_MAX 70 /* chars */ +#define SUMMARY_LENGTH_MIN 3 /* chars */ +#define WRITING_TIME_MIN 5 /* seconds */ + +struct _GsReviewDialog +{ + GtkDialog parent_instance; + + GtkWidget *star; + GtkWidget *label_rating_desc; + GtkWidget *summary_entry; + GtkWidget *post_button; + GtkWidget *text_view; + guint timer_id; +}; + +G_DEFINE_TYPE (GsReviewDialog, gs_review_dialog, GTK_TYPE_DIALOG) + +gint +gs_review_dialog_get_rating (GsReviewDialog *dialog) +{ + return gs_star_widget_get_rating (GS_STAR_WIDGET (dialog->star)); +} + +void +gs_review_dialog_set_rating (GsReviewDialog *dialog, gint rating) +{ + gs_star_widget_set_rating (GS_STAR_WIDGET (dialog->star), rating); +} + +const gchar * +gs_review_dialog_get_summary (GsReviewDialog *dialog) +{ + return gtk_entry_get_text (GTK_ENTRY (dialog->summary_entry)); +} + +gchar * +gs_review_dialog_get_text (GsReviewDialog *dialog) +{ + GtkTextBuffer *buffer; + GtkTextIter start, end; + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view)); + gtk_text_buffer_get_start_iter (buffer, &start); + gtk_text_buffer_get_end_iter (buffer, &end); + return gtk_text_buffer_get_text (buffer, &start, &end, FALSE); +} + +static void +gs_review_dialog_update_review_comment (GsReviewDialog *dialog) +{ + const gchar *msg = NULL; + gint perc; + + /* update the rating description */ + perc = gs_star_widget_get_rating (GS_STAR_WIDGET (dialog->star)); + if (perc == 20) { + /* TRANSLATORS: lighthearted star rating description; + * A really bad application */ + msg = _("Hate it"); + } else if (perc == 40) { + /* TRANSLATORS: lighthearted star rating description; + * Not a great application */ + msg = _("Don’t like it"); + } else if (perc == 60) { + /* TRANSLATORS: lighthearted star rating description; + * A fairly-good application */ + msg = _("It’s OK"); + } else if (perc == 80) { + /* TRANSLATORS: lighthearted star rating description; + * A good application */ + msg = _("Like it"); + } else if (perc == 100) { + /* TRANSLATORS: lighthearted star rating description; + * A really awesome application */ + msg = _("Love it"); + } else { + /* just reserve space */ + msg = ""; + } + gtk_label_set_label (GTK_LABEL (dialog->label_rating_desc), msg); +} + +static void +gs_review_dialog_changed_cb (GsReviewDialog *dialog) +{ + GtkTextBuffer *buffer; + gboolean all_okay = TRUE; + const gchar *msg = NULL; + + /* update review text */ + gs_review_dialog_update_review_comment (dialog); + + /* require rating, summary and long review */ + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view)); + if (dialog->timer_id != 0) { + /* TRANSLATORS: the review can't just be copied and pasted */ + msg = _("Please take more time writing the review"); + all_okay = FALSE; + } else if (gs_star_widget_get_rating (GS_STAR_WIDGET (dialog->star)) == 0) { + /* TRANSLATORS: the review is not acceptable */ + msg = _("Please choose a star rating"); + all_okay = FALSE; + } else if (gtk_entry_get_text_length (GTK_ENTRY (dialog->summary_entry)) < SUMMARY_LENGTH_MIN) { + /* TRANSLATORS: the review is not acceptable */ + msg = _("The summary is too short"); + all_okay = FALSE; + } else if (gtk_entry_get_text_length (GTK_ENTRY (dialog->summary_entry)) > SUMMARY_LENGTH_MAX) { + /* TRANSLATORS: the review is not acceptable */ + msg = _("The summary is too long"); + all_okay = FALSE; + } else if (gtk_text_buffer_get_char_count (buffer) < DESCRIPTION_LENGTH_MIN) { + /* TRANSLATORS: the review is not acceptable */ + msg = _("The description is too short"); + all_okay = FALSE; + } else if (gtk_text_buffer_get_char_count (buffer) > DESCRIPTION_LENGTH_MAX) { + /* TRANSLATORS: the review is not acceptable */ + msg = _("The description is too long"); + all_okay = FALSE; + } + + /* tell the user what's happening */ + gtk_widget_set_tooltip_text (dialog->post_button, msg); + + /* can the user submit this? */ + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, all_okay); +} + +static gboolean +gs_review_dialog_timeout_cb (gpointer user_data) +{ + GsReviewDialog *dialog = GS_REVIEW_DIALOG (user_data); + dialog->timer_id = 0; + gs_review_dialog_changed_cb (dialog); + return FALSE; +} + +static void +gs_review_dialog_init (GsReviewDialog *dialog) +{ + GtkTextBuffer *buffer; + gtk_widget_init_template (GTK_WIDGET (dialog)); + +#ifdef HAVE_GSPELL + /* allow checking spelling */ + { + GspellEntry *gspell_entry; + GspellTextView *gspell_view; + + gspell_entry = gspell_entry_get_from_gtk_entry (GTK_ENTRY (dialog->summary_entry)); + gspell_entry_basic_setup (gspell_entry); + + gspell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (dialog->text_view)); + gspell_text_view_basic_setup (gspell_view); + } +#endif + + /* require the user to spend at least 30 seconds on writing a review */ + dialog->timer_id = g_timeout_add_seconds (WRITING_TIME_MIN, + gs_review_dialog_timeout_cb, + dialog); + + /* update UI */ + g_signal_connect_swapped (dialog->star, "rating-changed", + G_CALLBACK (gs_review_dialog_changed_cb), dialog); + g_signal_connect_swapped (dialog->summary_entry, "notify::text", + G_CALLBACK (gs_review_dialog_changed_cb), dialog); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view)); + g_signal_connect_swapped (buffer, "changed", + G_CALLBACK (gs_review_dialog_changed_cb), dialog); + + gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_OK, FALSE); +} + +static void +gs_review_row_dispose (GObject *object) +{ + GsReviewDialog *dialog = GS_REVIEW_DIALOG (object); + if (dialog->timer_id > 0) { + g_source_remove (dialog->timer_id); + dialog->timer_id = 0; + } + G_OBJECT_CLASS (gs_review_dialog_parent_class)->dispose (object); +} + +static void +gs_review_dialog_class_init (GsReviewDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_review_row_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-review-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsReviewDialog, star); + gtk_widget_class_bind_template_child (widget_class, GsReviewDialog, label_rating_desc); + gtk_widget_class_bind_template_child (widget_class, GsReviewDialog, summary_entry); + gtk_widget_class_bind_template_child (widget_class, GsReviewDialog, text_view); + gtk_widget_class_bind_template_child (widget_class, GsReviewDialog, post_button); +} + +GtkWidget * +gs_review_dialog_new (void) +{ + return GTK_WIDGET (g_object_new (GS_TYPE_REVIEW_DIALOG, + "use-header-bar", TRUE, + NULL)); +} diff --git a/src/gs-review-dialog.h b/src/gs-review-dialog.h new file mode 100644 index 0000000..33d9992 --- /dev/null +++ b/src/gs-review-dialog.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_REVIEW_DIALOG (gs_review_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsReviewDialog, gs_review_dialog, GS, REVIEW_DIALOG, GtkDialog) + +GtkWidget *gs_review_dialog_new (void); +gint gs_review_dialog_get_rating (GsReviewDialog *dialog); +void gs_review_dialog_set_rating (GsReviewDialog *dialog, + gint rating); +const gchar *gs_review_dialog_get_summary (GsReviewDialog *dialog); +gchar *gs_review_dialog_get_text (GsReviewDialog *dialog); + +G_END_DECLS diff --git a/src/gs-review-dialog.ui b/src/gs-review-dialog.ui new file mode 100644 index 0000000..c79ee53 --- /dev/null +++ b/src/gs-review-dialog.ui @@ -0,0 +1,211 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.18.3 --> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsReviewDialog" parent="GtkDialog"> + <action-widgets> + <action-widget response="cancel">cancel_button</action-widget> + <action-widget response="ok" default="true">post_button</action-widget> + </action-widgets> + <property name="title" translatable="yes" comments="Translators: Title of the dialog box where the users can write and publish their opinions about the apps.">Post Review</property> + <property name="modal">True</property> + <property name="default_width">600</property> + <property name="default_height">300</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <property name="show_close_button">False</property> + <child> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + <child> + <object class="GtkButton" id="post_button"> + <property name="label" translatable="yes" comments="Translators: A button to publish the user's opinion about the app.">_Post</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox"> + <property name="margin_start">40</property> + <property name="margin_end">40</property> + <property name="margin_top">25</property> + <property name="margin_bottom">25</property> + <property name="orientation">vertical</property> + <property name="spacing">9</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + </object> + </child> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">20</property> + <property name="vexpand">True</property> + <child> + <object class="GtkBox" id="box4"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="label" translatable="yes">Rating</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GsStarWidget" id="star"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="icon-size">32</property> + <property name="interactive">True</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_rating_desc"> + <property name="height_request">30</property> + <property name="visible">True</property> + <property name="label"></property> + <property name="xalign">0.5</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box2"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Summary</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes">Give a short summary of your review, for example: “Great app, would recommend”.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkEntry" id="summary_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="box3"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="vexpand">True</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes" context="app review" comments="Translators: This is where the users enter their opinions about the apps.">Review</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="label" translatable="yes">What do you think of the app? Try to give reasons for your views.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkScrolledWindow" id="text_view_scroll"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <property name="vexpand">True</property> + <child> + <object class="GtkTextView" id="text_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="height-request">120</property> + <property name="wrap-mode">word-char</property> + <style> + <class name="review-textbox"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="label" translatable="yes">Find what data is sent in our <a href="https://odrs.gnome.org/privacy">privacy policy</a>.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + <property name="use-markup">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup" id="sizegroup_folder_buttons"> + <property name="ignore-hidden">False</property> + <property name="mode">horizontal</property> + <widgets> + <widget name="cancel_button"/> + <widget name="post_button"/> + </widgets> + </object> +</interface> diff --git a/src/gs-review-histogram.c b/src/gs-review-histogram.c new file mode 100644 index 0000000..5137f63 --- /dev/null +++ b/src/gs-review-histogram.c @@ -0,0 +1,112 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-review-histogram.h" +#include "gs-review-bar.h" + +typedef struct +{ + GtkWidget *bar1; + GtkWidget *bar2; + GtkWidget *bar3; + GtkWidget *bar4; + GtkWidget *bar5; + GtkWidget *label_count1; + GtkWidget *label_count2; + GtkWidget *label_count3; + GtkWidget *label_count4; + GtkWidget *label_count5; + GtkWidget *label_total; +} GsReviewHistogramPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsReviewHistogram, gs_review_histogram, GTK_TYPE_BIN) + +static void +set_label (GtkWidget *label, gint value) +{ + g_autofree gchar *text = NULL; + text = g_strdup_printf ("%i", value); + gtk_label_set_text (GTK_LABEL (label), text); +} + +void +gs_review_histogram_set_ratings (GsReviewHistogram *histogram, + GArray *review_ratings) +{ + GsReviewHistogramPrivate *priv = gs_review_histogram_get_instance_private (histogram); + gdouble fraction[6] = { 0.0f }; + guint32 max = 0; + guint32 total = 0; + + g_return_if_fail (GS_IS_REVIEW_HISTOGRAM (histogram)); + + /* sanity check */ + if (review_ratings->len != 6) { + g_warning ("ratings data incorrect expected 012345"); + return; + } + + /* idx 0 is '0 stars' which we don't support in the UI */ + for (guint i = 1; i < review_ratings->len; i++) { + guint32 c = g_array_index (review_ratings, guint32, i); + max = MAX (c, max); + } + for (guint i = 1; i < review_ratings->len; i++) { + guint32 c = g_array_index (review_ratings, guint32, i); + fraction[i] = max > 0 ? (gdouble) c / (gdouble) max : 0.f; + total += c; + } + + gs_review_bar_set_fraction (GS_REVIEW_BAR (priv->bar5), fraction[5]); + set_label (priv->label_count5, g_array_index (review_ratings, guint, 5)); + gs_review_bar_set_fraction (GS_REVIEW_BAR (priv->bar4), fraction[4]); + set_label (priv->label_count4, g_array_index (review_ratings, guint, 4)); + gs_review_bar_set_fraction (GS_REVIEW_BAR (priv->bar3), fraction[3]); + set_label (priv->label_count3, g_array_index (review_ratings, guint, 3)); + gs_review_bar_set_fraction (GS_REVIEW_BAR (priv->bar2), fraction[2]); + set_label (priv->label_count2, g_array_index (review_ratings, guint, 2)); + gs_review_bar_set_fraction (GS_REVIEW_BAR (priv->bar1), fraction[1]); + set_label (priv->label_count1, g_array_index (review_ratings, guint, 1)); + set_label (priv->label_total, total); +} + +static void +gs_review_histogram_init (GsReviewHistogram *histogram) +{ + gtk_widget_init_template (GTK_WIDGET (histogram)); +} + +static void +gs_review_histogram_class_init (GsReviewHistogramClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-review-histogram.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, bar5); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, bar4); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, bar3); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, bar2); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, bar1); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_count5); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_count4); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_count3); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_count2); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_count1); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewHistogram, label_total); +} + +GtkWidget * +gs_review_histogram_new (void) +{ + GsReviewHistogram *histogram; + histogram = g_object_new (GS_TYPE_REVIEW_HISTOGRAM, NULL); + return GTK_WIDGET (histogram); +} diff --git a/src/gs-review-histogram.h b/src/gs-review-histogram.h new file mode 100644 index 0000000..ada8073 --- /dev/null +++ b/src/gs-review-histogram.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_REVIEW_HISTOGRAM (gs_review_histogram_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsReviewHistogram, gs_review_histogram, GS, REVIEW_HISTOGRAM, GtkBin) + +struct _GsReviewHistogramClass +{ + GtkBinClass parent_class; +}; + +GtkWidget *gs_review_histogram_new (void); + +void gs_review_histogram_set_ratings (GsReviewHistogram *histogram, + GArray *review_ratings); + +G_END_DECLS diff --git a/src/gs-review-histogram.ui b/src/gs-review-histogram.ui new file mode 100644 index 0000000..11d4c6b --- /dev/null +++ b/src/gs-review-histogram.ui @@ -0,0 +1,424 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsReviewHistogram" parent="GtkBin"> + <property name="visible">True</property> + <child> + <object class="GtkGrid" id="grid1"> + <property name="visible">True</property> + <property name="row-spacing">6</property> + <property name="column-spacing">6</property> + <child> + <object class="GtkLabel" id="label_count5"> + <property name="halign">end</property> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GsReviewBar" id="bar5"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <property name="width-request">120</property> + <style> + <class name="reviewbar"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="histogram_star5_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="star5_1"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star5_2"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star5_3"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star5_4"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star5_5"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_count4"> + <property name="halign">end</property> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GsReviewBar" id="bar4"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <style> + <class name="reviewbar"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="histogram_star4_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="star4_1"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star4_2"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star4_3"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star4_4"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">1</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_count3"> + <property name="halign">end</property> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GsReviewBar" id="bar3"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <style> + <class name="reviewbar"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="histogram_star3_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="star3_1"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star3_2"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star3_3"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_count2"> + <property name="halign">end</property> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GsReviewBar" id="bar2"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <style> + <class name="reviewbar"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="histogram_star2_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="star2_1"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="star2_2"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_count1"> + <property name="halign">end</property> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">4</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GsReviewBar" id="bar1"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <style> + <class name="reviewbar"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">4</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="histogram_star1_box"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <property name="halign">start</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkImage" id="star1_1"> + <property name="visible">True</property> + <property name="icon_name">starred-symbolic</property> + <property name="sensitive">False</property> + <style> + <class name="star-disabled"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">4</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_total"> + <property name="visible">True</property> + <property name="label">0</property> + <property name="margin-left">5</property> + <property name="margin-top">5</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">5</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_1"> + <property name="visible">True</property> + <property name="margin-left">5</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="label" translatable="yes" comments="Translators: A label for the total number of reviews.">ratings in total</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">5</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-review-row.c b/src/gs-review-row.c new file mode 100644 index 0000000..a2b65ad --- /dev/null +++ b/src/gs-review-row.c @@ -0,0 +1,326 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-review-row.h" +#include "gs-star-widget.h" + +typedef struct +{ + GtkListBoxRow parent_instance; + + AsReview *review; + gboolean network_available; + guint64 actions; + GtkWidget *stars; + GtkWidget *summary_label; + GtkWidget *author_label; + GtkWidget *date_label; + GtkWidget *text_label; + GtkWidget *button_yes; + GtkWidget *button_no; + GtkWidget *button_dismiss; + GtkWidget *button_report; + GtkWidget *button_remove; + GtkWidget *box_voting; +} GsReviewRowPrivate; + +enum { + SIGNAL_BUTTON_CLICKED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +G_DEFINE_TYPE_WITH_PRIVATE (GsReviewRow, gs_review_row, GTK_TYPE_LIST_BOX_ROW) + +static void +gs_review_row_refresh (GsReviewRow *row) +{ + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (row); + const gchar *reviewer; + GDateTime *date; + g_autofree gchar *text = NULL; + + gs_star_widget_set_rating (GS_STAR_WIDGET (priv->stars), + as_review_get_rating (priv->review)); + reviewer = as_review_get_reviewer_name (priv->review); + if (reviewer == NULL) { + /* TRANSLATORS: this is when a user doesn't specify a name */ + reviewer = _("Unknown"); + } + gtk_label_set_text (GTK_LABEL (priv->author_label), reviewer); + date = as_review_get_date (priv->review); + if (date != NULL) + /* TRANSLATORS: This is the date string with: day number, month name, year. + i.e. "25 May 2012" */ + text = g_date_time_format (date, _("%e %B %Y")); + else + text = g_strdup (""); + gtk_label_set_text (GTK_LABEL (priv->date_label), text); + gtk_label_set_text (GTK_LABEL (priv->summary_label), + as_review_get_summary (priv->review)); + gtk_label_set_text (GTK_LABEL (priv->text_label), + as_review_get_description (priv->review)); + + /* if we voted, we can't do any actions */ + if (as_review_get_flags (priv->review) & AS_REVIEW_FLAG_VOTED) + priv->actions = 0; + + /* set actions up */ + if ((priv->actions & (1 << GS_PLUGIN_ACTION_REVIEW_UPVOTE | + 1 << GS_PLUGIN_ACTION_REVIEW_DOWNVOTE | + 1 << GS_PLUGIN_ACTION_REVIEW_DISMISS)) == 0) { + gtk_widget_set_visible (priv->box_voting, FALSE); + } else { + gtk_widget_set_visible (priv->box_voting, TRUE); + gtk_widget_set_visible (priv->button_yes, + priv->actions & 1 << GS_PLUGIN_ACTION_REVIEW_UPVOTE); + gtk_widget_set_visible (priv->button_no, + priv->actions & 1 << GS_PLUGIN_ACTION_REVIEW_DOWNVOTE); + gtk_widget_set_visible (priv->button_dismiss, + priv->actions & 1 << GS_PLUGIN_ACTION_REVIEW_DISMISS); + } + gtk_widget_set_visible (priv->button_remove, + priv->actions & 1 << GS_PLUGIN_ACTION_REVIEW_REMOVE); + gtk_widget_set_visible (priv->button_report, + priv->actions & 1 << GS_PLUGIN_ACTION_REVIEW_REPORT); + + /* mark insensitive if no network */ + if (priv->network_available) { + gtk_widget_set_sensitive (priv->button_yes, TRUE); + gtk_widget_set_sensitive (priv->button_no, TRUE); + gtk_widget_set_sensitive (priv->button_remove, TRUE); + gtk_widget_set_sensitive (priv->button_report, TRUE); + } else { + gtk_widget_set_sensitive (priv->button_yes, FALSE); + gtk_widget_set_sensitive (priv->button_no, FALSE); + gtk_widget_set_sensitive (priv->button_remove, FALSE); + gtk_widget_set_sensitive (priv->button_report, FALSE); + } +} + +void +gs_review_row_set_network_available (GsReviewRow *review_row, gboolean network_available) +{ + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (review_row); + priv->network_available = network_available; + gs_review_row_refresh (review_row); +} + +static gboolean +gs_review_row_refresh_idle (gpointer user_data) +{ + GsReviewRow *row = GS_REVIEW_ROW (user_data); + + gs_review_row_refresh (row); + + g_object_unref (row); + return G_SOURCE_REMOVE; +} + +static void +gs_review_row_notify_props_changed_cb (GsApp *app, + GParamSpec *pspec, + GsReviewRow *row) +{ + g_idle_add (gs_review_row_refresh_idle, g_object_ref (row)); +} + +static void +gs_review_row_init (GsReviewRow *row) +{ + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (row); + priv->network_available = TRUE; + gtk_widget_set_has_window (GTK_WIDGET (row), FALSE); + gtk_widget_init_template (GTK_WIDGET (row)); +} + +static void +gs_review_row_dispose (GObject *object) +{ + GsReviewRow *row = GS_REVIEW_ROW (object); + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (row); + + g_clear_object (&priv->review); + + G_OBJECT_CLASS (gs_review_row_parent_class)->dispose (object); +} + +static void +gs_review_row_class_init (GsReviewRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_review_row_dispose; + + signals [SIGNAL_BUTTON_CLICKED] = + g_signal_new ("button-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsReviewRowClass, button_clicked), + NULL, NULL, g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-review-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, stars); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, summary_label); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, author_label); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, date_label); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, text_label); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, button_yes); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, button_no); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, button_dismiss); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, button_report); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, button_remove); + gtk_widget_class_bind_template_child_private (widget_class, GsReviewRow, box_voting); +} + +static void +gs_review_row_button_clicked_upvote_cb (GtkButton *button, GsReviewRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0, + GS_PLUGIN_ACTION_REVIEW_UPVOTE); +} + +static void +gs_review_row_button_clicked_downvote_cb (GtkButton *button, GsReviewRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0, + GS_PLUGIN_ACTION_REVIEW_DOWNVOTE); +} + +static void +gs_review_row_confirm_cb (GtkDialog *dialog, gint response_id, GsReviewRow *row) +{ + if (response_id == GTK_RESPONSE_YES) { + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0, + GS_PLUGIN_ACTION_REVIEW_REPORT); + } + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +gs_review_row_button_clicked_report_cb (GtkButton *button, GsReviewRow *row) +{ + GtkWidget *dialog; + GtkWidget *toplevel; + GtkWidget *widget; + g_autoptr(GString) str = NULL; + + str = g_string_new (""); + + /* TRANSLATORS: we explain what the action is going to do */ + g_string_append (str, _("You can report reviews for abusive, rude, or " + "discriminatory behavior.")); + g_string_append (str, " "); + + /* TRANSLATORS: we ask the user if they really want to do this */ + g_string_append (str, _("Once reported, a review will be hidden until " + "it has been checked by an administrator.")); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (button)); + dialog = gtk_message_dialog_new (GTK_WINDOW (toplevel), + GTK_DIALOG_MODAL | + GTK_DIALOG_DESTROY_WITH_PARENT | + GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + "%s", + /* TRANSLATORS: window title when + * reporting a user-submitted review + * for moderation */ + _("Report Review?")); + widget = gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: button text when + * sending a review for moderation */ + _("Report"), + GTK_RESPONSE_YES); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), + "destructive-action"); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", str->str); + g_signal_connect (dialog, "response", + G_CALLBACK (gs_review_row_confirm_cb), row); + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +gs_review_row_button_clicked_dismiss_cb (GtkButton *button, GsReviewRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0, + GS_PLUGIN_ACTION_REVIEW_DISMISS); +} + +static void +gs_review_row_button_clicked_remove_cb (GtkButton *button, GsReviewRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0, + GS_PLUGIN_ACTION_REVIEW_REMOVE); +} + +AsReview * +gs_review_row_get_review (GsReviewRow *review_row) +{ + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (review_row); + return priv->review; +} + +void +gs_review_row_set_actions (GsReviewRow *review_row, guint64 actions) +{ + GsReviewRowPrivate *priv = gs_review_row_get_instance_private (review_row); + priv->actions = actions; + gs_review_row_refresh (review_row); +} + +/** + * gs_review_row_new: + * @review: The review to show + * + * Create a widget suitable for showing an application review. + * + * Return value: A new @GsReviewRow. + **/ +GtkWidget * +gs_review_row_new (AsReview *review) +{ + GsReviewRow *row; + GsReviewRowPrivate *priv; + + g_return_val_if_fail (AS_IS_REVIEW (review), NULL); + + row = g_object_new (GS_TYPE_REVIEW_ROW, NULL); + priv = gs_review_row_get_instance_private (row); + priv->review = g_object_ref (review); + g_signal_connect_object (priv->review, "notify::state", + G_CALLBACK (gs_review_row_notify_props_changed_cb), + row, 0); + g_signal_connect_object (priv->button_yes, "clicked", + G_CALLBACK (gs_review_row_button_clicked_upvote_cb), + row, 0); + g_signal_connect_object (priv->button_no, "clicked", + G_CALLBACK (gs_review_row_button_clicked_downvote_cb), + row, 0); + g_signal_connect_object (priv->button_dismiss, "clicked", + G_CALLBACK (gs_review_row_button_clicked_dismiss_cb), + row, 0); + g_signal_connect_object (priv->button_report, "clicked", + G_CALLBACK (gs_review_row_button_clicked_report_cb), + row, 0); + g_signal_connect_object (priv->button_remove, "clicked", + G_CALLBACK (gs_review_row_button_clicked_remove_cb), + row, 0); + gs_review_row_refresh (row); + + return GTK_WIDGET (row); +} diff --git a/src/gs-review-row.h b/src/gs-review-row.h new file mode 100644 index 0000000..8f952ba --- /dev/null +++ b/src/gs-review-row.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Canonical Ltd. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_REVIEW_ROW (gs_review_row_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsReviewRow, gs_review_row, GS, REVIEW_ROW, GtkListBoxRow) + +struct _GsReviewRowClass +{ + GtkListBoxRowClass parent_class; + void (*button_clicked) (GsReviewRow *review_row, + GsPluginAction action); +}; + +GtkWidget *gs_review_row_new (AsReview *review); +AsReview *gs_review_row_get_review (GsReviewRow *review_row); +void gs_review_row_set_actions (GsReviewRow *review_row, + guint64 actions); +void gs_review_row_set_network_available (GsReviewRow *review_row, + gboolean network_available); + +G_END_DECLS diff --git a/src/gs-review-row.ui b/src/gs-review-row.ui new file mode 100644 index 0000000..3ed3c4e --- /dev/null +++ b/src/gs-review-row.ui @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsReviewRow" parent="GtkListBoxRow"> + <property name="visible">True</property> + <property name="activatable">False</property> + <style> + <class name="review-row"/> + </style> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="margin_top">32</property> + <property name="row_spacing">6</property> + <property name="column_spacing">10</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkLabel" id="summary_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label">Steep learning curve, but worth it</property> + <property name="ellipsize">end</property> + <property name="hexpand">True</property> + <property name="selectable">True</property> + <style> + <class name="review-summary"/> + </style> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="date_label"> + <property name="visible">True</property> + <property name="halign">end</property> + <property name="label">3 January 2016</property> + <property name="hexpand">True</property> + <property name="selectable">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="author_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="label">Angela Avery</property> + <property name="selectable">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="text_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="margin_top">10</property> + <property name="margin_bottom">8</property> + <property name="label">Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used.</property> + <property name="wrap">True</property> + <property name="max_width_chars">80</property> + <property name="xalign">0</property> + <property name="wrap-mode">word-char</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GsStarWidget" id="stars"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_voting"> + <property name="visible">True</property> + <property name="spacing">9</property> + <property name="halign">start</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes" comments="Translators: Users can express their opinions about other users' opinions about the apps.">Was this review useful to you?</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="box_vote_buttons"> + <property name="visible">True</property> + <property name="spacing">0</property> + <property name="halign">start</property> + <style> + <class name="vote-buttons"/> + </style> + <child> + <object class="GtkButton" id="button_yes"> + <property name="label" translatable="yes">Yes</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_no"> + <property name="label" translatable="yes">No</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_dismiss"> + <property name="label" translatable="yes" comments="Translators: Button text for indifference, only used when moderating">Meh</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_action_buttons"> + <property name="visible">True</property> + <property name="spacing">9</property> + <property name="halign">end</property> + <child> + <object class="GtkButton" id="button_report"> + <property name="label" translatable="yes">Report…</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="relief">none</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="label" translatable="yes">Remove…</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="relief">none</property> + </object> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + </child> + + </template> + + <object class="GtkSizeGroup" id="action_sizegroup"> + <widgets> + <widget name="button_report"/> + <widget name="button_remove"/> + </widgets> + </object> + <object class="GtkSizeGroup" id="useful_sizegroup"> + <widgets> + <widget name="button_yes"/> + <widget name="button_no"/> + </widgets> + </object> + +</interface> diff --git a/src/gs-screenshot-image.c b/src/gs-screenshot-image.c new file mode 100644 index 0000000..0ecba07 --- /dev/null +++ b/src/gs-screenshot-image.c @@ -0,0 +1,592 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-screenshot-image.h" +#include "gs-common.h" + +struct _GsScreenshotImage +{ + GtkBin parent_instance; + + AsScreenshot *screenshot; + GtkWidget *stack; + GtkWidget *box_error; + GtkWidget *image1; + GtkWidget *image2; + GtkWidget *label_error; + GSettings *settings; + SoupSession *session; + SoupMessage *message; + gchar *filename; + const gchar *current_image; + guint width; + guint height; + guint scale; + gboolean showing_image; +}; + +G_DEFINE_TYPE (GsScreenshotImage, gs_screenshot_image, GTK_TYPE_BIN) + +AsScreenshot * +gs_screenshot_image_get_screenshot (GsScreenshotImage *ssimg) +{ + g_return_val_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg), NULL); + return ssimg->screenshot; +} + +static void +gs_screenshot_image_set_error (GsScreenshotImage *ssimg, const gchar *message) +{ + gint width, height; + + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "error"); + gtk_label_set_label (GTK_LABEL (ssimg->label_error), message); + gtk_widget_get_size_request (ssimg->stack, &width, &height); + if (width < 200) + gtk_widget_hide (ssimg->label_error); + else + gtk_widget_show (ssimg->label_error); + ssimg->showing_image = FALSE; +} + +static void +as_screenshot_show_image (GsScreenshotImage *ssimg) +{ + g_autoptr(GdkPixbuf) pixbuf = NULL; + + /* no need to composite */ + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT) { + pixbuf = gdk_pixbuf_new_from_file (ssimg->filename, NULL); + } else { + /* this is always going to have alpha */ + pixbuf = gdk_pixbuf_new_from_file_at_scale (ssimg->filename, + (gint) (ssimg->width * ssimg->scale), + (gint) (ssimg->height * ssimg->scale), + FALSE, NULL); + } + + /* show icon */ + if (g_strcmp0 (ssimg->current_image, "image1") == 0) { + if (pixbuf != NULL) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image2), + pixbuf, (gint) ssimg->scale); + } + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "image2"); + ssimg->current_image = "image2"; + } else { + if (pixbuf != NULL) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image1), + pixbuf, (gint) ssimg->scale); + } + gtk_stack_set_visible_child_name (GTK_STACK (ssimg->stack), "image1"); + ssimg->current_image = "image1"; + } + + gtk_widget_show (GTK_WIDGET (ssimg)); + ssimg->showing_image = TRUE; +} + +static void +gs_screenshot_image_show_blurred (GsScreenshotImage *ssimg, + const gchar *filename_thumb) +{ + g_autoptr(AsImage) im = NULL; + g_autoptr(GdkPixbuf) pb = NULL; + + /* create an helper which can do the blurring for us */ + im = as_image_new (); + if (!as_image_load_filename (im, filename_thumb, NULL)) + return; + pb = as_image_save_pixbuf (im, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale, + AS_IMAGE_SAVE_FLAG_BLUR); + if (pb == NULL) + return; + + if (g_strcmp0 (ssimg->current_image, "image1") == 0) { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image1), + pb, (gint) ssimg->scale); + } else { + gs_image_set_from_pixbuf_with_scale (GTK_IMAGE (ssimg->image2), + pb, (gint) ssimg->scale); + } +} + +static gboolean +gs_screenshot_image_save_downloaded_img (GsScreenshotImage *ssimg, + GdkPixbuf *pixbuf, + GError **error) +{ + g_autoptr(AsImage) im = NULL; + gboolean ret; + const GPtrArray *images; + g_autoptr(GError) error_local = NULL; + g_autofree char *filename = NULL; + g_autofree char *size_dir = NULL; + g_autofree char *cache_kind = NULL; + g_autofree char *basename = NULL; + guint width = ssimg->width; + guint height = ssimg->height; + + /* save to file, using the same code as the AppStream builder + * so the preview looks the same */ + im = as_image_new (); + as_image_set_pixbuf (im, pixbuf); + ret = as_image_save_filename (im, ssimg->filename, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale, + AS_IMAGE_SAVE_FLAG_PAD_16_9, + error); + + if (!ret) + return FALSE; + + if (ssimg->screenshot == NULL) + return TRUE; + + images = as_screenshot_get_images (ssimg->screenshot); + if (images->len > 1) + return TRUE; + + if (width == AS_IMAGE_THUMBNAIL_WIDTH && + height == AS_IMAGE_THUMBNAIL_HEIGHT) { + width = AS_IMAGE_NORMAL_WIDTH; + height = AS_IMAGE_NORMAL_HEIGHT; + } else { + width = AS_IMAGE_THUMBNAIL_WIDTH; + height = AS_IMAGE_THUMBNAIL_HEIGHT; + } + + width *= ssimg->scale; + height *= ssimg->scale; + basename = g_path_get_basename (ssimg->filename); + size_dir = g_strdup_printf ("%ux%u", width, height); + cache_kind = g_build_filename ("screenshots", size_dir, NULL); + filename = gs_utils_get_cache_filename (cache_kind, basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + &error_local); + + if (filename == NULL) { + /* if we cannot get a cache filename, warn about that but do not + * set a user's visible error because this is a complementary + * operation */ + g_warning ("Failed to get cache filename for counterpart " + "screenshot '%s' in folder '%s': %s", basename, + cache_kind, error_local->message); + return TRUE; + } + + ret = as_image_save_filename (im, filename, width, height, + AS_IMAGE_SAVE_FLAG_PAD_16_9, + &error_local); + + if (!ret) { + /* if we cannot save this screenshot, warn about that but do not + * set a user's visible error because this is a complementary + * operation */ + g_warning ("Failed to save screenshot '%s': %s", filename, + error_local->message); + } + + return TRUE; +} + +static void +gs_screenshot_image_complete_cb (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + g_autoptr(GsScreenshotImage) ssimg = GS_SCREENSHOT_IMAGE (user_data); + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GInputStream) stream = NULL; + + /* return immediately if the message was cancelled or if we're in destruction */ + if (msg->status_code == SOUP_STATUS_CANCELLED || ssimg->session == NULL) + return; + + if (msg->status_code == SOUP_STATUS_NOT_MODIFIED) { + g_debug ("screenshot has not been modified"); + as_screenshot_show_image (ssimg); + return; + } + if (msg->status_code != SOUP_STATUS_OK) { + g_warning ("Result of screenshot downloading attempt with " + "status code '%u': %s", msg->status_code, + msg->reason_phrase); + /* if we're already showing an image, then don't set the error + * as having an image (even if outdated) is better */ + if (ssimg->showing_image) + return; + /* TRANSLATORS: this is when we try to download a screenshot and + * we get back 404 */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not found")); + return; + } + + /* create a buffer with the data */ + stream = g_memory_input_stream_new_from_data (msg->response_body->data, + msg->response_body->length, + NULL); + if (stream == NULL) + return; + + /* load the image */ + pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, NULL); + if (pixbuf == NULL) { + /* TRANSLATORS: possibly image file corrupt or not an image */ + gs_screenshot_image_set_error (ssimg, _("Failed to load image")); + return; + } + + /* is image size destination size unknown or exactly the correct size */ + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT || + (ssimg->width * ssimg->scale == (guint) gdk_pixbuf_get_width (pixbuf) && + ssimg->height * ssimg->scale == (guint) gdk_pixbuf_get_height (pixbuf))) { + ret = g_file_set_contents (ssimg->filename, + msg->response_body->data, + msg->response_body->length, + &error); + if (!ret) { + gs_screenshot_image_set_error (ssimg, error->message); + return; + } + } else if (!gs_screenshot_image_save_downloaded_img (ssimg, pixbuf, + &error)) { + gs_screenshot_image_set_error (ssimg, error->message); + return; + } + + /* got image, so show */ + as_screenshot_show_image (ssimg); +} + +void +gs_screenshot_image_set_screenshot (GsScreenshotImage *ssimg, + AsScreenshot *screenshot) +{ + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + g_return_if_fail (AS_IS_SCREENSHOT (screenshot)); + + if (ssimg->screenshot == screenshot) + return; + if (ssimg->screenshot) + g_object_unref (ssimg->screenshot); + ssimg->screenshot = g_object_ref (screenshot); + + /* we reset this flag here too because it referred to the previous + * screenshot, and thus avoids potentially assuming that the new + * screenshot is shown when it is the previous one instead */ + ssimg->showing_image = FALSE; +} + +void +gs_screenshot_image_set_size (GsScreenshotImage *ssimg, + guint width, guint height) +{ + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + g_return_if_fail (width != 0); + g_return_if_fail (height != 0); + + ssimg->width = width; + ssimg->height = height; + gtk_widget_set_size_request (ssimg->stack, (gint) width, (gint) height); +} + +static gchar * +gs_screenshot_get_cachefn_for_url (const gchar *url) +{ + g_autofree gchar *basename = NULL; + g_autofree gchar *checksum = NULL; + checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA256, url, -1); + basename = g_path_get_basename (url); + return g_strdup_printf ("%s-%s", checksum, basename); +} + +static void +gs_screenshot_soup_msg_set_modified_request (SoupMessage *msg, GFile *file) +{ +#ifndef GLIB_VERSION_2_62 + GTimeVal time_val; +#endif + g_autoptr(GDateTime) date_time = NULL; + g_autoptr(GFileInfo) info = NULL; + g_autofree gchar *mod_date = NULL; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info == NULL) + return; +#ifdef GLIB_VERSION_2_62 + date_time = g_file_info_get_modification_date_time (info); +#else + g_file_info_get_modification_time (info, &time_val); + date_time = g_date_time_new_from_timeval_local (&time_val); +#endif + mod_date = g_date_time_format (date_time, "%a, %d %b %Y %H:%M:%S %Z"); + soup_message_headers_append (msg->request_headers, + "If-Modified-Since", + mod_date); +} + +void +gs_screenshot_image_load_async (GsScreenshotImage *ssimg, + GCancellable *cancellable) +{ + AsImage *im = NULL; + const gchar *url; + g_autofree gchar *basename = NULL; + g_autofree gchar *cache_kind = NULL; + g_autofree gchar *cachefn_thumb = NULL; + g_autofree gchar *sizedir = NULL; + g_autoptr(SoupURI) base_uri = NULL; + + g_return_if_fail (GS_IS_SCREENSHOT_IMAGE (ssimg)); + + g_return_if_fail (AS_IS_SCREENSHOT (ssimg->screenshot)); + g_return_if_fail (ssimg->width != 0); + g_return_if_fail (ssimg->height != 0); + + /* load an image according to the scale factor */ + ssimg->scale = (guint) gtk_widget_get_scale_factor (GTK_WIDGET (ssimg)); + im = as_screenshot_get_image (ssimg->screenshot, + ssimg->width * ssimg->scale, + ssimg->height * ssimg->scale); + + /* if we've failed to load a HiDPI image, fallback to LoDPI */ + if (im == NULL && ssimg->scale > 1) { + ssimg->scale = 1; + im = as_screenshot_get_image (ssimg->screenshot, + ssimg->width, + ssimg->height); + } + if (im == NULL) { + /* TRANSLATORS: this is when we request a screenshot size that + * the generator did not create or the parser did not add */ + gs_screenshot_image_set_error (ssimg, _("Screenshot size not found")); + return; + } + + /* check if the URL points to a local file */ + url = as_image_get_url (im); + if (g_str_has_prefix (url, "file://")) { + g_free (ssimg->filename); + ssimg->filename = g_strdup (url + 7); + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + as_screenshot_show_image (ssimg); + return; + } + } + + basename = gs_screenshot_get_cachefn_for_url (url); + if (ssimg->width == G_MAXUINT || ssimg->height == G_MAXUINT) { + sizedir = g_strdup ("unknown"); + } else { + sizedir = g_strdup_printf ("%ux%u", ssimg->width * ssimg->scale, ssimg->height * ssimg->scale); + } + cache_kind = g_build_filename ("screenshots", sizedir, NULL); + g_free (ssimg->filename); + ssimg->filename = gs_utils_get_cache_filename (cache_kind, + basename, + GS_UTILS_CACHE_FLAG_NONE, + NULL); + if (ssimg->filename == NULL) { + /* TRANSLATORS: this is when we try create the cache directory + * but we were out of space or permission was denied */ + gs_screenshot_image_set_error (ssimg, _("Could not create cache")); + return; + } + + /* does local file already exist and has recently been downloaded */ + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + guint64 age_max; + g_autoptr(GFile) file = NULL; + + /* show the image we have in cache while we're checking for the + * new screenshot (which probably won't have changed) */ + as_screenshot_show_image (ssimg); + + /* verify the cache age against the maximum allowed */ + age_max = g_settings_get_uint (ssimg->settings, + "screenshot-cache-age-maximum"); + file = g_file_new_for_path (ssimg->filename); + /* image new enough, not re-requesting from server */ + if (age_max > 0 && gs_utils_get_file_age (file) < age_max) + return; + } + + /* if we're not showing a full-size image, we try loading a blurred + * smaller version of it straight away */ + if (!ssimg->showing_image && + ssimg->width > AS_IMAGE_THUMBNAIL_WIDTH && + ssimg->height > AS_IMAGE_THUMBNAIL_HEIGHT) { + const gchar *url_thumb; + g_autofree gchar *basename_thumb = NULL; + g_autofree gchar *cache_kind_thumb = NULL; + im = as_screenshot_get_image (ssimg->screenshot, + AS_IMAGE_THUMBNAIL_WIDTH * ssimg->scale, + AS_IMAGE_THUMBNAIL_HEIGHT * ssimg->scale); + url_thumb = as_image_get_url (im); + basename_thumb = gs_screenshot_get_cachefn_for_url (url_thumb); + cache_kind_thumb = g_build_filename ("screenshots", "112x63", NULL); + cachefn_thumb = gs_utils_get_cache_filename (cache_kind_thumb, + basename_thumb, + GS_UTILS_CACHE_FLAG_NONE, + NULL); + if (cachefn_thumb == NULL) + return; + if (g_file_test (cachefn_thumb, G_FILE_TEST_EXISTS)) + gs_screenshot_image_show_blurred (ssimg, cachefn_thumb); + } + + /* re-request the cache filename, which might be different as it needs + * to be writable this time */ + g_free (ssimg->filename); + ssimg->filename = gs_utils_get_cache_filename (cache_kind, + basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + NULL); + + /* download file */ + g_debug ("downloading %s to %s", url, ssimg->filename); + base_uri = soup_uri_new (url); + if (base_uri == NULL || !SOUP_URI_VALID_FOR_HTTP (base_uri)) { + /* TRANSLATORS: this is when we try to download a screenshot + * that was not a valid URL */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not valid")); + return; + } + + /* cancel any previous messages */ + if (ssimg->message != NULL) { + soup_session_cancel_message (ssimg->session, + ssimg->message, + SOUP_STATUS_CANCELLED); + g_clear_object (&ssimg->message); + } + + ssimg->message = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri); + if (ssimg->message == NULL) { + /* TRANSLATORS: this is when networking is not available */ + gs_screenshot_image_set_error (ssimg, _("Screenshot not available")); + return; + } + + /* not all servers support If-Modified-Since, but worst case we just + * re-download the entire file again every 30 days */ + if (g_file_test (ssimg->filename, G_FILE_TEST_EXISTS)) { + g_autoptr(GFile) file = g_file_new_for_path (ssimg->filename); + gs_screenshot_soup_msg_set_modified_request (ssimg->message, file); + } + + /* send async */ + soup_session_queue_message (ssimg->session, + g_object_ref (ssimg->message) /* transfer full */, + gs_screenshot_image_complete_cb, + g_object_ref (ssimg)); +} + +gboolean +gs_screenshot_image_is_showing (GsScreenshotImage *ssimg) +{ + return ssimg->showing_image; +} + +static void +gs_screenshot_image_destroy (GtkWidget *widget) +{ + GsScreenshotImage *ssimg = GS_SCREENSHOT_IMAGE (widget); + + if (ssimg->message != NULL) { + soup_session_cancel_message (ssimg->session, + ssimg->message, + SOUP_STATUS_CANCELLED); + g_clear_object (&ssimg->message); + } + g_clear_object (&ssimg->screenshot); + g_clear_object (&ssimg->session); + g_clear_object (&ssimg->settings); + + g_clear_pointer (&ssimg->filename, g_free); + + GTK_WIDGET_CLASS (gs_screenshot_image_parent_class)->destroy (widget); +} + +static void +gs_screenshot_image_init (GsScreenshotImage *ssimg) +{ + AtkObject *accessible; + + ssimg->settings = g_settings_new ("org.gnome.software"); + ssimg->showing_image = FALSE; + + gtk_widget_set_has_window (GTK_WIDGET (ssimg), FALSE); + gtk_widget_init_template (GTK_WIDGET (ssimg)); + + accessible = gtk_widget_get_accessible (GTK_WIDGET (ssimg)); + if (accessible != 0) { + atk_object_set_role (accessible, ATK_ROLE_IMAGE); + atk_object_set_name (accessible, _("Screenshot")); + } +} + +static gboolean +gs_screenshot_image_draw (GtkWidget *widget, cairo_t *cr) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (widget); + gtk_render_background (context, cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + gtk_render_frame (context, cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + + return GTK_WIDGET_CLASS (gs_screenshot_image_parent_class)->draw (widget, cr); +} + +static void +gs_screenshot_image_class_init (GsScreenshotImageClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = gs_screenshot_image_destroy; + widget_class->draw = gs_screenshot_image_draw; + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/Software/gs-screenshot-image.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, stack); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, image1); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, image2); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, box_error); + gtk_widget_class_bind_template_child (widget_class, GsScreenshotImage, label_error); +} + +GtkWidget * +gs_screenshot_image_new (SoupSession *session) +{ + GsScreenshotImage *ssimg; + ssimg = g_object_new (GS_TYPE_SCREENSHOT_IMAGE, NULL); + ssimg->session = g_object_ref (session); + return GTK_WIDGET (ssimg); +} diff --git a/src/gs-screenshot-image.h b/src/gs-screenshot-image.h new file mode 100644 index 0000000..a5d9355 --- /dev/null +++ b/src/gs-screenshot-image.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> +#include <libsoup/soup.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_SCREENSHOT_IMAGE (gs_screenshot_image_get_type ()) + +G_DECLARE_FINAL_TYPE (GsScreenshotImage, gs_screenshot_image, GS, SCREENSHOT_IMAGE, GtkBin) + +GtkWidget *gs_screenshot_image_new (SoupSession *session); + +AsScreenshot *gs_screenshot_image_get_screenshot (GsScreenshotImage *ssimg); +void gs_screenshot_image_set_screenshot (GsScreenshotImage *ssimg, + AsScreenshot *screenshot); +void gs_screenshot_image_set_size (GsScreenshotImage *ssimg, + guint width, + guint height); +void gs_screenshot_image_load_async (GsScreenshotImage *ssimg, + GCancellable *cancellable); +gboolean gs_screenshot_image_is_showing (GsScreenshotImage *ssimg); + +G_END_DECLS diff --git a/src/gs-screenshot-image.ui b/src/gs-screenshot-image.ui new file mode 100644 index 0000000..5b20671 --- /dev/null +++ b/src/gs-screenshot-image.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsScreenshotImage" parent="GtkBin"> + <property name="visible">True</property> + <style> + <class name="screenshot-image"/> + </style> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="transition-type">crossfade</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + <packing> + <property name="name">image1</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + <packing> + <property name="name">image2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_error"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + <child> + <object class="GtkImage" id="image_error"> + <property name="visible">True</property> + <property name="icon-name">dialog-error-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_error"> + <property name="visible">True</property> + <style> + <class name="error-label"/> + </style> + </object> + </child> + </object> + <packing> + <property name="name">error</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-search-page.c b/src/gs-search-page.c new file mode 100644 index 0000000..8b7b6c1 --- /dev/null +++ b/src/gs-search-page.c @@ -0,0 +1,504 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-search-page.h" +#include "gs-shell.h" +#include "gs-common.h" +#include "gs-app-row.h" + +#define GS_SEARCH_PAGE_MAX_RESULTS 50 + +struct _GsSearchPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + GCancellable *search_cancellable; + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + GsShell *shell; + gchar *appid_to_show; + gchar *value; + guint waiting_id; + guint max_results; + + GtkWidget *list_box_search; + GtkWidget *scrolledwindow_search; + GtkWidget *spinner_search; + GtkWidget *stack_search; +}; + +G_DEFINE_TYPE (GsSearchPage, gs_search_page, GS_TYPE_PAGE) + +static void +gs_search_page_app_row_clicked_cb (GsAppRow *app_row, + GsSearchPage *self) +{ + GsApp *app; + app = gs_app_row_get_app (app_row); + if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE) + gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL, + self->cancellable); + else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) + gs_page_remove_app (GS_PAGE (self), app, self->cancellable); + else if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) { + if (gs_app_get_url (app, AS_URL_KIND_MISSING) == NULL) { + gs_page_install_app (GS_PAGE (self), app, + GS_SHELL_INTERACTION_FULL, + self->cancellable); + return; + } + gs_shell_show_uri (self->shell, + gs_app_get_url (app, AS_URL_KIND_MISSING)); + } +} + +static void +gs_search_page_waiting_cancel (GsSearchPage *self) +{ + if (self->waiting_id > 0) + g_source_remove (self->waiting_id); + self->waiting_id = 0; +} + +static void +gs_search_page_get_search_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + guint i; + GsApp *app; + GsSearchPage *self = GS_SEARCH_PAGE (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + GtkWidget *app_row; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* don't do the delayed spinner */ + gs_search_page_waiting_cancel (self); + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("search cancelled"); + return; + } + g_warning ("failed to get search apps: %s", error->message); + gs_stop_spinner (GTK_SPINNER (self->spinner_search)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-results"); + return; + } + + /* no results */ + if (gs_app_list_length (list) == 0) { + g_debug ("no search results to show"); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-results"); + return; + } + + /* remove old entries */ + gs_container_remove_all (GTK_CONTAINER (self->list_box_search)); + + gs_stop_spinner (GTK_SPINNER (self->spinner_search)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "results"); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + app_row = gs_app_row_new (app); + gs_app_row_set_show_rating (GS_APP_ROW (app_row), TRUE); + g_signal_connect (app_row, "button-clicked", + G_CALLBACK (gs_search_page_app_row_clicked_cb), + self); + gtk_container_add (GTK_CONTAINER (self->list_box_search), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button); + gtk_widget_show (app_row); + } + + /* too many results */ + if (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)) { + GtkStyleContext *context; + GtkWidget *w = gtk_label_new (NULL); + g_autofree gchar *str = NULL; + + /* TRANSLATORS: this is when there are too many search results + * to show in in the search page */ + str = g_strdup_printf (ngettext("%u more match", + "%u more matches", + gs_app_list_get_size_peak (list) - gs_app_list_length (list)), + gs_app_list_get_size_peak (list) - gs_app_list_length (list)); + gtk_label_set_label (GTK_LABEL (w), str); + gtk_widget_set_margin_bottom (w, 20); + gtk_widget_set_margin_top (w, 20); + gtk_widget_set_margin_start (w, 20); + gtk_widget_set_margin_end (w, 20); + context = gtk_widget_get_style_context (w); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL); + gtk_container_add (GTK_CONTAINER (self->list_box_search), w); + gtk_widget_show (w); + } else { + /* reset to default */ + self->max_results = GS_SEARCH_PAGE_MAX_RESULTS; + } + + if (self->appid_to_show != NULL) { + g_autoptr (GsApp) a = NULL; + if (as_utils_unique_id_valid (self->appid_to_show)) { + a = gs_plugin_loader_app_create (self->plugin_loader, + self->appid_to_show); + } else { + a = gs_app_new (self->appid_to_show); + } + gs_shell_show_app (self->shell, a); + g_clear_pointer (&self->appid_to_show, g_free); + } +} + +static gboolean +gs_search_page_waiting_show_cb (gpointer user_data) +{ + GsSearchPage *self = GS_SEARCH_PAGE (user_data); + + /* show spinner */ + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "spinner"); + gs_start_spinner (GTK_SPINNER (self->spinner_search)); + gs_search_page_waiting_cancel (self); + return FALSE; +} + +static gchar * +gs_search_page_get_app_sort_key (GsApp *app) +{ + GString *key = g_string_sized_new (64); + + /* sort apps before runtimes and extensions */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_DESKTOP: + case AS_APP_KIND_SHELL_EXTENSION: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort missing codecs before applications */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_UNAVAILABLE: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort by the search key */ + g_string_append_printf (key, "%05x:", gs_app_get_match_value (app)); + + /* sort by rating */ + g_string_append_printf (key, "%03i:", gs_app_get_rating (app)); + + /* sort by kudos */ + g_string_append_printf (key, "%03u:", gs_app_get_kudos_percentage (app)); + + return g_string_free (key, FALSE); +} + +static gboolean +gs_search_page_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + g_autofree gchar *key1 = NULL; + g_autofree gchar *key2 = NULL; + key1 = gs_search_page_get_app_sort_key (app1); + key2 = gs_search_page_get_app_sort_key (app2); + return g_strcmp0 (key2, key1); +} + +static void +gs_search_page_load (GsSearchPage *self) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* cancel any pending searches */ + g_cancellable_cancel (self->search_cancellable); + g_clear_object (&self->search_cancellable); + self->search_cancellable = g_cancellable_new (); + + /* search for apps */ + gs_search_page_waiting_cancel (self); + self->waiting_id = g_timeout_add (250, gs_search_page_waiting_show_cb, self); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", self->value, + "max-results", self->max_results, + "timeout", 10, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_job_set_sort_func (plugin_job, gs_search_page_sort_cb); + gs_plugin_job_set_sort_func_data (plugin_job, self); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->search_cancellable, + gs_search_page_get_search_cb, + self); +} + +static void +gs_search_page_app_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsSearchPage *self) +{ + GsApp *app; + + /* increase the maximum allowed, and re-request the search */ + if (!GS_IS_APP_ROW (row)) { + self->max_results *= 4; + gs_search_page_load (self); + return; + } + + app = gs_app_row_get_app (GS_APP_ROW (row)); + gs_shell_show_app (self->shell, app); +} + +static void +gs_search_page_reload (GsPage *page) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + if (self->value != NULL) + gs_search_page_load (self); +} + +/** + * gs_search_page_set_appid_to_show: + * + * Switch to the specified app id after loading the search results. + **/ +void +gs_search_page_set_appid_to_show (GsSearchPage *self, const gchar *appid) +{ + g_free (self->appid_to_show); + self->appid_to_show = g_strdup (appid); +} + +const gchar * +gs_search_page_get_text (GsSearchPage *self) +{ + return self->value; +} + +void +gs_search_page_set_text (GsSearchPage *self, const gchar *value) +{ + if (value == self->value) + return; + if (g_strcmp0 (value, self->value) == 0) + return; + + g_free (self->value); + self->value = g_strdup (value); + + gs_search_page_load (self); +} + +static void +gs_search_page_switch_to (GsPage *page, gboolean scroll_up) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + GtkWidget *widget; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_SEARCH) { + g_warning ("Called switch_to(search) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "buttonbox_main")); + gtk_widget_show (widget); + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "menu_button")); + gtk_widget_show (widget); + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "search_bar")); + gtk_widget_show (widget); + + /* hardcode */ + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "search_button")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE); + + if (scroll_up) { + GtkAdjustment *adj; + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_search)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } +} + +static void +gs_search_page_list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header; + + /* first entry */ + header = gtk_list_box_row_get_header (row); + if (before == NULL) { + gtk_list_box_row_set_header (row, NULL); + return; + } + + /* already set */ + if (header != NULL) + return; + + /* set new */ + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static void +gs_search_page_cancel_cb (GCancellable *cancellable, + GsSearchPage *self) +{ + g_cancellable_cancel (self->search_cancellable); +} + +static void +gs_search_page_app_installed (GsPage *page, GsApp *app) +{ + gs_search_page_reload (page); +} + +static void +gs_search_page_app_removed (GsPage *page, GsApp *app) +{ + gs_search_page_reload (page); +} + +static gboolean +gs_search_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + + g_return_val_if_fail (GS_IS_SEARCH_PAGE (self), TRUE); + + self->plugin_loader = g_object_ref (plugin_loader); + self->builder = g_object_ref (builder); + self->cancellable = g_object_ref (cancellable); + self->shell = shell; + + /* connect the cancellables */ + g_cancellable_connect (self->cancellable, + G_CALLBACK (gs_search_page_cancel_cb), + self, NULL); + + /* setup search */ + g_signal_connect (self->list_box_search, "row-activated", + G_CALLBACK (gs_search_page_app_row_activated_cb), self); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_search), + gs_search_page_list_header_func, + self, NULL); + return TRUE; +} + +static void +gs_search_page_dispose (GObject *object) +{ + GsSearchPage *self = GS_SEARCH_PAGE (object); + + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + + g_clear_object (&self->builder); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + g_clear_object (&self->search_cancellable); + + G_OBJECT_CLASS (gs_search_page_parent_class)->dispose (object); +} + +static void +gs_search_page_finalize (GObject *object) +{ + GsSearchPage *self = GS_SEARCH_PAGE (object); + + g_free (self->appid_to_show); + g_free (self->value); + + G_OBJECT_CLASS (gs_search_page_parent_class)->finalize (object); +} + +static void +gs_search_page_class_init (GsSearchPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_search_page_dispose; + object_class->finalize = gs_search_page_finalize; + page_class->app_installed = gs_search_page_app_installed; + page_class->app_removed = gs_search_page_app_removed; + page_class->switch_to = gs_search_page_switch_to; + page_class->reload = gs_search_page_reload; + page_class->setup = gs_search_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-search-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, list_box_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, scrolledwindow_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, spinner_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, stack_search); +} + +static void +gs_search_page_init (GsSearchPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + self->max_results = GS_SEARCH_PAGE_MAX_RESULTS; +} + +GsSearchPage * +gs_search_page_new (void) +{ + GsSearchPage *self; + self = g_object_new (GS_TYPE_SEARCH_PAGE, NULL); + return GS_SEARCH_PAGE (self); +} diff --git a/src/gs-search-page.h b/src/gs-search-page.h new file mode 100644 index 0000000..f955a21 --- /dev/null +++ b/src/gs-search-page.h @@ -0,0 +1,27 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_SEARCH_PAGE (gs_search_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsSearchPage, gs_search_page, GS, SEARCH_PAGE, GsPage) + +GsSearchPage *gs_search_page_new (void); +void gs_search_page_set_appid_to_show (GsSearchPage *self, + const gchar *appid); +const gchar *gs_search_page_get_text (GsSearchPage *self); +void gs_search_page_set_text (GsSearchPage *self, + const gchar *value); + +G_END_DECLS diff --git a/src/gs-search-page.ui b/src/gs-search-page.ui new file mode 100644 index 0000000..b150030 --- /dev/null +++ b/src/gs-search-page.ui @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsSearchPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="search-accessible"> + <property name="accessible-name" translatable="yes">Search page</property> + </object> + </child> + <child> + <object class="GtkStack" id="stack_search"> + <property name="visible">True</property> + <child> + <object class="GtkSpinner" id="spinner_search"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="noresults_grid_search"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="row-spacing">12</property> + <property name="column-spacing">12</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="noresults_icon"> + <property name="visible">True</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + <property name="pixel-size">64</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="noresults_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">No Application Found</property> + <property name="halign">start</property> + <property name="valign">center</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="name">no-results</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_search"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <property name="valign">start</property> + <child> + <object class="GtkListBox" id="list_box_search"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">results</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-self-test.c b/src/gs-self-test.c new file mode 100644 index 0000000..43d94c5 --- /dev/null +++ b/src/gs-self-test.c @@ -0,0 +1,61 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-content-rating.h" +#include "gs-css.h" +#include "gs-test.h" + +static void +gs_css_func (void) +{ + const gchar *tmp; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsCss) css = gs_css_new (); + + /* no IDs */ + ret = gs_css_parse (css, "border: 0;", &error); + g_assert_no_error (error); + g_assert (ret); + tmp = gs_css_get_markup_for_id (css, "tile"); + g_assert_cmpstr (tmp, ==, "border: 0;"); + + /* with IDs */ + ret = gs_css_parse (css, "#tile2{\nborder: 0;}\n#name {color: white;\n}", &error); + g_assert_no_error (error); + g_assert (ret); + tmp = gs_css_get_markup_for_id (css, "NotGoingToExist"); + g_assert_cmpstr (tmp, ==, NULL); + tmp = gs_css_get_markup_for_id (css, "tile2"); + g_assert_cmpstr (tmp, ==, "border: 0;"); + tmp = gs_css_get_markup_for_id (css, "name"); + g_assert_cmpstr (tmp, ==, "color: white;"); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, +#if GLIB_CHECK_VERSION(2, 60, 0) + G_TEST_OPTION_ISOLATE_DIRS, +#endif + NULL); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + + /* tests go here */ + g_test_add_func ("/gnome-software/src/css", gs_css_func); + + return g_test_run (); +} diff --git a/src/gs-shell-search-provider.c b/src/gs-shell-search-provider.c new file mode 100644 index 0000000..715f70f --- /dev/null +++ b/src/gs-shell-search-provider.c @@ -0,0 +1,403 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * gs-shell-search-provider.c - Implementation of a GNOME Shell + * search provider + * + * Copyright (C) 2013 Matthias Clasen + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gio/gio.h> +#include <glib/gi18n.h> +#include <string.h> + +#include "gs-shell-search-provider-generated.h" +#include "gs-shell-search-provider.h" +#include "gs-common.h" + +#define GS_SHELL_SEARCH_PROVIDER_MAX_RESULTS 20 + +typedef struct { + GsShellSearchProvider *provider; + GDBusMethodInvocation *invocation; +} PendingSearch; + +struct _GsShellSearchProvider { + GObject parent; + + GsShellSearchProvider2 *skeleton; + GsPluginLoader *plugin_loader; + GCancellable *cancellable; + + GHashTable *metas_cache; + GsAppList *search_results; +}; + +G_DEFINE_TYPE (GsShellSearchProvider, gs_shell_search_provider, G_TYPE_OBJECT) + +static void +pending_search_free (PendingSearch *search) +{ + g_object_unref (search->invocation); + g_slice_free (PendingSearch, search); +} + +static gint +search_sort_by_kudo_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + guint pa, pb; + pa = gs_app_get_kudos_percentage (app1); + pb = gs_app_get_kudos_percentage (app2); + if (pa < pb) + return 1; + else if (pa > pb) + return -1; + return 0; +} + +static void +search_done_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + PendingSearch *search = user_data; + GsShellSearchProvider *self = search->provider; + guint i; + GVariantBuilder builder; + g_autoptr(GsAppList) list = NULL; + + /* cache no longer valid */ + gs_app_list_remove_all (self->search_results); + + list = gs_plugin_loader_job_process_finish (self->plugin_loader, res, NULL); + if (list == NULL) { + g_dbus_method_invocation_return_value (search->invocation, g_variant_new ("(as)", NULL)); + pending_search_free (search); + g_application_release (g_application_get_default ()); + return; + } + + /* sort by kudos, as there is no ratings data by default */ + gs_app_list_sort (list, search_sort_by_kudo_cb, NULL); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (gs_app_get_state (app) != AS_APP_STATE_AVAILABLE) + continue; + g_variant_builder_add (&builder, "s", gs_app_get_unique_id (app)); + + /* cache this in case we need the app in GetResultMetas */ + gs_app_list_add (self->search_results, app); + } + g_dbus_method_invocation_return_value (search->invocation, g_variant_new ("(as)", &builder)); + + pending_search_free (search); + g_application_release (g_application_get_default ()); +} + +static gchar * +gs_shell_search_provider_get_app_sort_key (GsApp *app) +{ + GString *key = g_string_sized_new (64); + + /* sort available apps before installed ones */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_AVAILABLE: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort apps before runtimes and extensions */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_DESKTOP: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort by the search key */ + g_string_append_printf (key, "%05x:", gs_app_get_match_value (app)); + + /* tie-break with id */ + g_string_append (key, gs_app_get_unique_id (app)); + + return g_string_free (key, FALSE); +} + +static gboolean +gs_shell_search_provider_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + g_autofree gchar *key1 = NULL; + g_autofree gchar *key2 = NULL; + key1 = gs_shell_search_provider_get_app_sort_key (app1); + key2 = gs_shell_search_provider_get_app_sort_key (app2); + return g_strcmp0 (key2, key1); +} + +static void +execute_search (GsShellSearchProvider *self, + GDBusMethodInvocation *invocation, + gchar **terms) +{ + PendingSearch *pending_search; + g_autofree gchar *value = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + value = g_strjoinv (" ", terms); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + /* don't attempt searches for a single character */ + if (g_strv_length (terms) == 1 && + g_utf8_strlen (terms[0], -1) == 1) { + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(as)", NULL)); + return; + } + + pending_search = g_slice_new (PendingSearch); + pending_search->provider = self; + pending_search->invocation = g_object_ref (invocation); + + g_application_hold (g_application_get_default ()); + self->cancellable = g_cancellable_new (); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", value, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME, + "max-results", GS_SHELL_SEARCH_PROVIDER_MAX_RESULTS, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + NULL); + gs_plugin_job_set_sort_func (plugin_job, gs_shell_search_provider_sort_cb); + gs_plugin_job_set_sort_func_data (plugin_job, self); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + search_done_cb, + pending_search); +} + +static gboolean +handle_get_initial_result_set (GsShellSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + gchar **terms, + gpointer user_data) +{ + GsShellSearchProvider *self = user_data; + + g_debug ("****** GetInitialResultSet"); + execute_search (self, invocation, terms); + return TRUE; +} + +static gboolean +handle_get_subsearch_result_set (GsShellSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + gchar **previous_results, + gchar **terms, + gpointer user_data) +{ + GsShellSearchProvider *self = user_data; + + g_debug ("****** GetSubSearchResultSet"); + execute_search (self, invocation, terms); + return TRUE; +} + +static gboolean +handle_get_result_metas (GsShellSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + gchar **results, + gpointer user_data) +{ + GsShellSearchProvider *self = user_data; + GVariantBuilder meta; + GVariant *meta_variant; + GdkPixbuf *pixbuf; + gint i; + GVariantBuilder builder; + + g_debug ("****** GetResultMetas"); + + for (i = 0; results[i]; i++) { + GsApp *app; + g_autofree gchar *description = NULL; + + /* already built */ + if (g_hash_table_lookup (self->metas_cache, results[i]) != NULL) + continue; + + /* get previously found app */ + app = gs_app_list_lookup (self->search_results, results[i]); + if (app == NULL) { + g_warning ("failed to refine find app %s in cache", results[i]); + continue; + } + + g_variant_builder_init (&meta, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&meta, "{sv}", "id", g_variant_new_string (gs_app_get_unique_id (app))); + g_variant_builder_add (&meta, "{sv}", "name", g_variant_new_string (gs_app_get_name (app))); + pixbuf = gs_app_get_pixbuf (app); + if (pixbuf != NULL) + g_variant_builder_add (&meta, "{sv}", "icon", g_icon_serialize (G_ICON (pixbuf))); + + if (gs_utils_list_has_app_fuzzy (self->search_results, app) && + gs_app_get_origin_hostname (app) != NULL) { + /* TRANSLATORS: this refers to where the app came from */ + g_autofree gchar *source_text = g_strdup_printf (_("Source: %s"), + gs_app_get_origin_hostname (app)); + description = g_strdup_printf ("%s %s", + gs_app_get_summary (app), + source_text); + } else { + description = g_strdup (gs_app_get_summary (app)); + } + g_variant_builder_add (&meta, "{sv}", "description", g_variant_new_string (description)); + + meta_variant = g_variant_builder_end (&meta); + g_hash_table_insert (self->metas_cache, + g_strdup (gs_app_get_unique_id (app)), + g_variant_ref_sink (meta_variant)); + + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + for (i = 0; results[i]; i++) { + meta_variant = (GVariant*)g_hash_table_lookup (self->metas_cache, results[i]); + if (meta_variant == NULL) + continue; + g_variant_builder_add_value (&builder, meta_variant); + } + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(aa{sv})", &builder)); + + return TRUE; +} + +static gboolean +handle_activate_result (GsShellSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + gchar *result, + gchar **terms, + guint32 timestamp, + gpointer user_data) +{ + GApplication *app = g_application_get_default (); + g_autofree gchar *string = NULL; + + string = g_strjoinv (" ", terms); + + g_action_group_activate_action (G_ACTION_GROUP (app), "details", + g_variant_new ("(ss)", result, string)); + + gs_shell_search_provider2_complete_activate_result (skeleton, invocation); + return TRUE; +} + +static gboolean +handle_launch_search (GsShellSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + gchar **terms, + guint32 timestamp, + gpointer user_data) +{ + GApplication *app = g_application_get_default (); + g_autofree gchar *string = g_strjoinv (" ", terms); + + g_action_group_activate_action (G_ACTION_GROUP (app), "search", + g_variant_new ("s", string)); + + gs_shell_search_provider2_complete_launch_search (skeleton, invocation); + return TRUE; +} + +gboolean +gs_shell_search_provider_register (GsShellSearchProvider *self, + GDBusConnection *connection, + GError **error) +{ + return g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton), + connection, + "/org/gnome/Software/SearchProvider", error); +} + +void +gs_shell_search_provider_unregister (GsShellSearchProvider *self) +{ + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton)); +} + +static void +search_provider_dispose (GObject *obj) +{ + GsShellSearchProvider *self = GS_SHELL_SEARCH_PROVIDER (obj); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + if (self->metas_cache != NULL) { + g_hash_table_destroy (self->metas_cache); + self->metas_cache = NULL; + } + + g_clear_object (&self->search_results); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->skeleton); + + G_OBJECT_CLASS (gs_shell_search_provider_parent_class)->dispose (obj); +} + +static void +gs_shell_search_provider_init (GsShellSearchProvider *self) +{ + self->metas_cache = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash, + (GEqualFunc) as_utils_unique_id_equal, + g_free, + (GDestroyNotify) g_variant_unref); + + self->search_results = gs_app_list_new (); + self->skeleton = gs_shell_search_provider2_skeleton_new (); + + g_signal_connect (self->skeleton, "handle-get-initial-result-set", + G_CALLBACK (handle_get_initial_result_set), self); + g_signal_connect (self->skeleton, "handle-get-subsearch-result-set", + G_CALLBACK (handle_get_subsearch_result_set), self); + g_signal_connect (self->skeleton, "handle-get-result-metas", + G_CALLBACK (handle_get_result_metas), self); + g_signal_connect (self->skeleton, "handle-activate-result", + G_CALLBACK (handle_activate_result), self); + g_signal_connect (self->skeleton, "handle-launch-search", + G_CALLBACK (handle_launch_search), self); +} + +static void +gs_shell_search_provider_class_init (GsShellSearchProviderClass *klass) +{ + GObjectClass *oclass = G_OBJECT_CLASS (klass); + + oclass->dispose = search_provider_dispose; +} + +GsShellSearchProvider * +gs_shell_search_provider_new (void) +{ + return g_object_new (gs_shell_search_provider_get_type (), NULL); +} + +void +gs_shell_search_provider_setup (GsShellSearchProvider *provider, + GsPluginLoader *loader) +{ + provider->plugin_loader = g_object_ref (loader); +} diff --git a/src/gs-shell-search-provider.h b/src/gs-shell-search-provider.h new file mode 100644 index 0000000..f3bbbd1 --- /dev/null +++ b/src/gs-shell-search-provider.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * gs-shell-search-provider.h - Implementation of a GNOME Shell + * search provider + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gnome-software-private.h" + +#define GS_TYPE_SHELL_SEARCH_PROVIDER gs_shell_search_provider_get_type() + +G_DECLARE_FINAL_TYPE (GsShellSearchProvider, gs_shell_search_provider, GS, SHELL_SEARCH_PROVIDER, GObject) + +gboolean gs_shell_search_provider_register (GsShellSearchProvider *self, + GDBusConnection *connection, + GError **error); +void gs_shell_search_provider_unregister (GsShellSearchProvider *self); +GsShellSearchProvider *gs_shell_search_provider_new (void); +void gs_shell_search_provider_setup (GsShellSearchProvider *provider, + GsPluginLoader *loader); diff --git a/src/gs-shell.c b/src/gs-shell.c new file mode 100644 index 0000000..8f0dc6f --- /dev/null +++ b/src/gs-shell.c @@ -0,0 +1,2532 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#ifdef HAVE_MOGWAI +#include <libmogwai-schedule-client/scheduler.h> +#endif + +#include "gs-common.h" +#include "gs-shell.h" +#include "gs-basic-auth-dialog.h" +#include "gs-details-page.h" +#include "gs-installed-page.h" +#include "gs-metered-data-dialog.h" +#include "gs-moderate-page.h" +#include "gs-loading-page.h" +#include "gs-search-page.h" +#include "gs-overview-page.h" +#include "gs-updates-page.h" +#include "gs-category-page.h" +#include "gs-extras-page.h" +#include "gs-repos-dialog.h" +#include "gs-prefs-dialog.h" +#include "gs-update-dialog.h" +#include "gs-update-monitor.h" +#include "gs-utils.h" + +static const gchar *page_name[] = { + "unknown", + "overview", + "installed", + "search", + "updates", + "details", + "category", + "extras", + "moderate", + "loading", +}; + +typedef struct { + GsShellMode mode; + GtkWidget *focus; + GsCategory *category; + gchar *search; +} BackEntry; + +typedef struct +{ + GSettings *settings; + gboolean ignore_primary_buttons; + GCancellable *cancellable; + GsPluginLoader *plugin_loader; + GsShellMode mode; + GHashTable *pages; + GtkWidget *header_start_widget; + GtkWidget *header_end_widget; + GtkBuilder *builder; + GtkWindow *main_window; + GQueue *back_entry_stack; + GPtrArray *modal_dialogs; + gulong search_changed_id; + gchar *events_info_uri; + gboolean in_mode_change; + GsPage *page; + +#ifdef HAVE_MOGWAI + MwscScheduler *scheduler; + gulong scheduler_invalidated_handler; +#endif /* HAVE_MOGWAI */ +} GsShellPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsShell, gs_shell, G_TYPE_OBJECT) + +enum { + SIGNAL_LOADED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +static void +modal_dialog_unmapped_cb (GtkWidget *dialog, + GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_debug ("modal dialog %p unmapped", dialog); + g_ptr_array_remove (priv->modal_dialogs, dialog); +} + +void +gs_shell_modal_dialog_present (GsShell *shell, GtkDialog *dialog) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWindow *parent; + + /* show new modal on top of old modal */ + if (priv->modal_dialogs->len > 0) { + parent = g_ptr_array_index (priv->modal_dialogs, + priv->modal_dialogs->len - 1); + g_debug ("using old modal %p as parent", parent); + } else { + parent = priv->main_window; + g_debug ("using main window"); + } + gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); + + /* add to stack, transfer ownership to here */ + g_ptr_array_add (priv->modal_dialogs, dialog); + g_signal_connect (GTK_WIDGET (dialog), "unmap", + G_CALLBACK (modal_dialog_unmapped_cb), shell); + + /* present the new one */ + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_present (GTK_WINDOW (dialog)); +} + +gboolean +gs_shell_is_active (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + return gtk_window_is_active (priv->main_window); +} + +GtkWindow * +gs_shell_get_window (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + return priv->main_window; +} + +void +gs_shell_activate (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + gtk_window_present (priv->main_window); +} + +static void +gs_shell_set_header_start_widget (GsShell *shell, GtkWidget *widget) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *old_widget; + GtkWidget *header; + + old_widget = priv->header_start_widget; + header = GTK_WIDGET (gtk_builder_get_object (priv->builder, "header")); + + if (priv->header_start_widget == widget) + return; + + if (widget != NULL) { + g_object_ref (widget); + gtk_header_bar_pack_start (GTK_HEADER_BAR (header), widget); + } + + priv->header_start_widget = widget; + + if (old_widget != NULL) { + gtk_container_remove (GTK_CONTAINER (header), old_widget); + g_object_unref (old_widget); + } +} + +static void +gs_shell_set_header_end_widget (GsShell *shell, GtkWidget *widget) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *old_widget; + GtkWidget *header; + + old_widget = priv->header_end_widget; + header = GTK_WIDGET (gtk_builder_get_object (priv->builder, "header")); + + if (priv->header_end_widget == widget) + return; + + if (widget != NULL) { + g_object_ref (widget); + gtk_header_bar_pack_end (GTK_HEADER_BAR (header), widget); + } + + priv->header_end_widget = widget; + + if (old_widget != NULL) { + gtk_container_remove (GTK_CONTAINER (header), old_widget); + g_object_unref (old_widget); + } +} + +static void +gs_shell_refresh_auto_updates_ui (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + gboolean automatic_updates_paused; + gboolean automatic_updates_enabled; + GtkInfoBar *metered_updates_bar; + + automatic_updates_enabled = g_settings_get_boolean (priv->settings, "download-updates"); + + metered_updates_bar = GTK_INFO_BAR (gtk_builder_get_object (priv->builder, "metered_updates_bar")); + +#ifdef HAVE_MOGWAI + automatic_updates_paused = (priv->scheduler == NULL || !mwsc_scheduler_get_allow_downloads (priv->scheduler)); +#else + automatic_updates_paused = gs_plugin_loader_get_network_metered (priv->plugin_loader); +#endif + + gtk_info_bar_set_revealed (metered_updates_bar, + priv->mode != GS_SHELL_MODE_LOADING && + automatic_updates_enabled && + automatic_updates_paused); + gtk_info_bar_set_default_response (metered_updates_bar, GTK_RESPONSE_OK); +} + +static void +gs_shell_metered_updates_bar_response_cb (GtkInfoBar *info_bar, + gint response_id, + gpointer user_data) +{ + GsShell *shell = GS_SHELL (user_data); + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkDialog *dialog; + + dialog = GTK_DIALOG (gs_metered_data_dialog_new (priv->main_window)); + gs_shell_modal_dialog_present (shell, dialog); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +static void +gs_shell_download_updates_changed_cb (GSettings *settings, + const gchar *key, + gpointer user_data) +{ + GsShell *shell = user_data; + + gs_shell_refresh_auto_updates_ui (shell); +} + +static void +gs_shell_network_metered_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + gpointer user_data) +{ +#ifndef HAVE_MOGWAI + GsShell *shell = user_data; + + /* @automatic_updates_paused only depends on network-metered if we’re + * compiled without Mogwai. */ + gs_shell_refresh_auto_updates_ui (shell); +#endif +} + +#ifdef HAVE_MOGWAI +static void +scheduler_invalidated_cb (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + /* The scheduler shouldn’t normally be invalidated, since we Hold() it + * until we’re done with it. However, if the scheduler is stopped by + * systemd (`systemctl stop mogwai-scheduled`) this signal will be + * emitted. It may also be invalidated while our main window is hidden, + * as we release our Hold() then. */ + g_signal_handler_disconnect (priv->scheduler, + priv->scheduler_invalidated_handler); + priv->scheduler_invalidated_handler = 0; + + g_clear_object (&priv->scheduler); +} + +static void +scheduler_allow_downloads_changed_cb (GsShell *shell) +{ + gs_shell_refresh_auto_updates_ui (shell); +} + +static void +scheduler_hold_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GError) error_local = NULL; + MwscScheduler *scheduler = (MwscScheduler *) source_object; + g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + if (!mwsc_scheduler_hold_finish (scheduler, result, &error_local)) { + g_warning ("Couldn't hold the Mogwai Scheduler daemon: %s", + error_local->message); + return; + } + + priv->scheduler_invalidated_handler = + g_signal_connect_swapped (scheduler, "invalidated", + (GCallback) scheduler_invalidated_cb, + shell); + + g_signal_connect_object (scheduler, "notify::allow-downloads", + (GCallback) scheduler_allow_downloads_changed_cb, + shell, + G_CONNECT_SWAPPED); + + g_assert (priv->scheduler == NULL); + priv->scheduler = scheduler; + + /* Update the UI accordingly. */ + gs_shell_refresh_auto_updates_ui (shell); +} + +static void +scheduler_release_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + MwscScheduler *scheduler = (MwscScheduler *) source_object; + g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_autoptr(GError) error_local = NULL; + + if (!mwsc_scheduler_release_finish (scheduler, result, &error_local)) + g_warning ("Couldn't release the Mogwai Scheduler daemon: %s", + error_local->message); + + g_clear_object (&priv->scheduler); +} + +static void +scheduler_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + MwscScheduler *scheduler; + g_autoptr(GError) error_local = NULL; + g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */ + + scheduler = mwsc_scheduler_new_finish (result, &error_local); + + if (scheduler == NULL) { + g_warning ("%s: Error getting Mogwai Scheduler: %s", G_STRFUNC, + error_local->message); + return; + } + + mwsc_scheduler_hold_async (scheduler, + "monitoring allow-downloads property", + NULL, + scheduler_hold_cb, + g_object_ref (shell)); +} +#endif /* HAVE_MOGWAI */ + +static void +gs_shell_basic_auth_start_cb (GsPluginLoader *plugin_loader, + const gchar *remote, + const gchar *realm, + GsBasicAuthCallback callback, + gpointer callback_data, + GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *dialog; + + dialog = gs_basic_auth_dialog_new (priv->main_window, remote, realm, callback, callback_data); + gs_shell_modal_dialog_present (shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +static void +free_back_entry (BackEntry *entry) +{ + if (entry->focus != NULL) + g_object_remove_weak_pointer (G_OBJECT (entry->focus), + (gpointer *) &entry->focus); + g_clear_object (&entry->category); + g_free (entry->search); + g_free (entry); +} + +static void +gs_shell_clean_back_entry_stack (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + BackEntry *entry; + + while ((entry = g_queue_pop_head (priv->back_entry_stack)) != NULL) { + free_back_entry (entry); + } +} + +void +gs_shell_change_mode (GsShell *shell, + GsShellMode mode, + gpointer data, + gboolean scroll_up) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GsApp *app; + GsPage *page; + GtkWidget *widget; + GtkStyleContext *context; + + if (priv->ignore_primary_buttons) + return; + + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "header")); + gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (widget), TRUE); + + /* hide all mode specific header widgets here, they will be shown in the + * refresh functions + */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "application_details_header")); + gtk_widget_hide (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "buttonbox_main")); + gtk_widget_hide (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "menu_button")); + gtk_widget_hide (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "header_selection_menu_button")); + gtk_widget_hide (widget); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "origin_box")); + gtk_widget_hide (widget); + + priv->in_mode_change = TRUE; + /* only show the search button in overview and search pages */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_button")); + gtk_widget_set_visible (widget, mode == GS_SHELL_MODE_OVERVIEW || + mode == GS_SHELL_MODE_SEARCH); + /* hide unless we're going to search */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_bar")); + gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (widget), + mode == GS_SHELL_MODE_SEARCH); + priv->in_mode_change = FALSE; + + context = gtk_widget_get_style_context (GTK_WIDGET (gtk_builder_get_object (priv->builder, "header"))); + gtk_style_context_remove_class (context, "selection-mode"); + + /* set the window title back to default */ + gtk_window_set_title (priv->main_window, g_get_application_name ()); + + /* update main buttons according to mode */ + priv->ignore_primary_buttons = TRUE; + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_explore")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), mode == GS_SHELL_MODE_OVERVIEW); + + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_installed")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), mode == GS_SHELL_MODE_INSTALLED); + + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_updates")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), mode == GS_SHELL_MODE_UPDATES); + gtk_widget_set_visible (widget, gs_plugin_loader_get_allow_updates (priv->plugin_loader) || + mode == GS_SHELL_MODE_UPDATES); + + priv->ignore_primary_buttons = FALSE; + + /* switch page */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "stack_main")); + gtk_stack_set_visible_child_name (GTK_STACK (widget), page_name[mode]); + + /* do action for mode */ + priv->mode = mode; + switch (mode) { + case GS_SHELL_MODE_OVERVIEW: + gs_shell_clean_back_entry_stack (shell); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "overview")); + break; + case GS_SHELL_MODE_INSTALLED: + gs_shell_clean_back_entry_stack (shell); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "installed")); + break; + case GS_SHELL_MODE_MODERATE: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "moderate")); + break; + case GS_SHELL_MODE_LOADING: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "loading")); + break; + case GS_SHELL_MODE_SEARCH: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "search")); + gs_search_page_set_text (GS_SEARCH_PAGE (page), data); + break; + case GS_SHELL_MODE_UPDATES: + gs_shell_clean_back_entry_stack (shell); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "updates")); + break; + case GS_SHELL_MODE_DETAILS: + app = GS_APP (data); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "details")); + if (gs_app_get_local_file (app) != NULL) { + gs_details_page_set_local_file (GS_DETAILS_PAGE (page), + gs_app_get_local_file (app)); + } else if (gs_app_get_metadata_item (app, "GnomeSoftware::from-url") != NULL) { + gs_details_page_set_url (GS_DETAILS_PAGE (page), + gs_app_get_metadata_item (app, "GnomeSoftware::from-url")); + } else { + gs_details_page_set_app (GS_DETAILS_PAGE (page), data); + } + break; + case GS_SHELL_MODE_CATEGORY: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "category")); + gs_category_page_set_category (GS_CATEGORY_PAGE (page), + GS_CATEGORY (data)); + break; + case GS_SHELL_MODE_EXTRAS: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "extras")); + break; + default: + g_assert_not_reached (); + } + + /* show the back button if needed */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_back")); + gtk_widget_set_visible (widget, + mode != GS_SHELL_MODE_SEARCH && + !g_queue_is_empty (priv->back_entry_stack)); + + priv->in_mode_change = TRUE; + + if (priv->page != NULL) + gs_page_switch_from (priv->page); + g_set_object (&priv->page, page); + gs_page_switch_to (page, scroll_up); + priv->in_mode_change = FALSE; + + /* update header bar widgets */ + widget = gs_page_get_header_start_widget (page); + gs_shell_set_header_start_widget (shell, widget); + + widget = gs_page_get_header_end_widget (page); + gs_shell_set_header_end_widget (shell, widget); + + /* refresh the updates bar when moving out of the loading mode, but only + * if the Mogwai scheduler state is already known, to avoid spuriously + * showing the updates bar */ +#ifdef HAVE_MOGWAI + if (priv->scheduler != NULL) +#else + if (TRUE) +#endif + gs_shell_refresh_auto_updates_ui (shell); + + /* destroy any existing modals */ + if (priv->modal_dialogs != NULL) { + gsize i = 0; + /* block signal emission of 'unmapped' since that will + * call g_ptr_array_remove_index. The unmapped signal may + * be emitted whilst running unref handlers for + * g_ptr_array_set_size */ + for (i = 0; i < priv->modal_dialogs->len; ++i) { + GtkWidget *dialog = g_ptr_array_index (priv->modal_dialogs, i); + g_signal_handlers_disconnect_by_func (dialog, + modal_dialog_unmapped_cb, + shell); + } + g_ptr_array_set_size (priv->modal_dialogs, 0); + } +} + +static void +gs_overview_page_button_cb (GtkWidget *widget, GsShell *shell) +{ + GsShellMode mode; + mode = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), + "gnome-software::overview-mode")); + gs_shell_change_mode (shell, mode, NULL, TRUE); +} + +static void +save_back_entry (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + BackEntry *entry; + GsPage *page; + + entry = g_new0 (BackEntry, 1); + entry->mode = priv->mode; + + entry->focus = gtk_window_get_focus (priv->main_window); + if (entry->focus != NULL) + g_object_add_weak_pointer (G_OBJECT (entry->focus), + (gpointer *) &entry->focus); + + switch (priv->mode) { + case GS_SHELL_MODE_CATEGORY: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "category")); + entry->category = gs_category_page_get_category (GS_CATEGORY_PAGE (page)); + g_object_ref (entry->category); + g_debug ("pushing back entry for %s with %s", + page_name[entry->mode], + gs_category_get_id (entry->category)); + break; + case GS_SHELL_MODE_SEARCH: + page = GS_PAGE (g_hash_table_lookup (priv->pages, "search")); + entry->search = g_strdup (gs_search_page_get_text (GS_SEARCH_PAGE (page))); + g_debug ("pushing back entry for %s with %s", + page_name[entry->mode], entry->search); + break; + default: + g_debug ("pushing back entry for %s", page_name[entry->mode]); + break; + } + + g_queue_push_head (priv->back_entry_stack, entry); +} + +static void +gs_shell_plugin_events_sources_cb (GtkWidget *widget, GsShell *shell) +{ + gs_shell_show_sources (shell); +} + +static void +gs_shell_plugin_events_no_space_cb (GtkWidget *widget, GsShell *shell) +{ + g_autoptr(GError) error = NULL; + if (!g_spawn_command_line_async ("baobab", &error)) + g_warning ("failed to exec baobab: %s", error->message); +} + +static void +gs_shell_plugin_events_network_settings_cb (GtkWidget *widget, GsShell *shell) +{ + g_autoptr(GError) error = NULL; + if (!g_spawn_command_line_async ("gnome-control-center network", &error)) + g_warning ("failed to exec gnome-control-center: %s", error->message); +} + +static void +gs_shell_plugin_events_more_info_cb (GtkWidget *widget, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_autoptr(GError) error = NULL; + if (!g_app_info_launch_default_for_uri (priv->events_info_uri, NULL, &error)) { + g_warning ("failed to launch URI %s: %s", + priv->events_info_uri, error->message); + } +} + +static void +gs_shell_plugin_events_restart_required_cb (GtkWidget *widget, GsShell *shell) +{ + g_autoptr(GError) error = NULL; + if (!g_spawn_command_line_async (LIBEXECDIR "/gnome-software-restarter", &error)) + g_warning ("failed to restart: %s", error->message); +} + +/* this is basically a workaround for GtkSearchEntry. Due to delayed emission of the search-changed + * signal it can't be blocked during insertion of text into the entry. Therefore we block the + * precursor of that signal to be able to add text to the entry without firing the handlers + * connected to "search-changed" + */ +static void +block_changed (GtkEditable *editable, + gpointer user_data) +{ + g_signal_stop_emission_by_name (editable, "changed"); +} + +static void +block_changed_signal (GtkSearchEntry *entry) +{ + g_signal_connect (entry, "changed", G_CALLBACK (block_changed), NULL); +} + +static void +unblock_changed_signal (GtkSearchEntry *entry) +{ + g_signal_handlers_disconnect_by_func (entry, G_CALLBACK (block_changed), NULL); +} + +static void +gs_shell_go_back (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + BackEntry *entry; + GtkWidget *widget; + + /* nothing to do */ + if (g_queue_is_empty (priv->back_entry_stack)) { + g_debug ("no back stack, showing overview"); + gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE); + return; + } + + entry = g_queue_pop_head (priv->back_entry_stack); + + switch (entry->mode) { + case GS_SHELL_MODE_UNKNOWN: + case GS_SHELL_MODE_LOADING: + /* happens when using --search, --details, --install, etc. options */ + g_debug ("popping back entry for %s", page_name[entry->mode]); + gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE); + break; + case GS_SHELL_MODE_CATEGORY: + g_debug ("popping back entry for %s with %s", + page_name[entry->mode], + gs_category_get_id (entry->category)); + gs_shell_change_mode (shell, entry->mode, entry->category, FALSE); + break; + case GS_SHELL_MODE_SEARCH: + g_debug ("popping back entry for %s with %s", + page_name[entry->mode], entry->search); + + /* set the text in the entry and move cursor to the end */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "entry_search")); + block_changed_signal (GTK_SEARCH_ENTRY (widget)); + gtk_entry_set_text (GTK_ENTRY (widget), entry->search); + gtk_editable_set_position (GTK_EDITABLE (widget), -1); + unblock_changed_signal (GTK_SEARCH_ENTRY (widget)); + + /* set the mode directly */ + gs_shell_change_mode (shell, entry->mode, + (gpointer) entry->search, FALSE); + break; + default: + g_debug ("popping back entry for %s", page_name[entry->mode]); + gs_shell_change_mode (shell, entry->mode, NULL, FALSE); + break; + } + + if (entry->focus != NULL) + gtk_widget_grab_focus (entry->focus); + + free_back_entry (entry); +} + +static void +gs_shell_back_button_cb (GtkWidget *widget, GsShell *shell) +{ + gs_shell_go_back (shell); +} + +static void +gs_shell_reload_cb (GsPluginLoader *plugin_loader, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_autoptr(GList) keys = g_hash_table_get_keys (priv->pages); + for (GList *l = keys; l != NULL; l = l->next) { + GsPage *page = GS_PAGE (g_hash_table_lookup (priv->pages, l->data)); + gs_page_reload (page); + } +} + +static void +overview_page_refresh_done (GsOverviewPage *overview_page, gpointer data) +{ + GsShell *shell = data; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GsPage *page; + + g_signal_handlers_disconnect_by_func (overview_page, overview_page_refresh_done, data); + + page = GS_PAGE (gtk_builder_get_object (priv->builder, "updates_page")); + gs_page_reload (page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "installed_page")); + gs_page_reload (page); + + gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, TRUE); + + /* now that we're finished with the loading page, connect the reload signal handler */ + g_signal_connect (priv->plugin_loader, "reload", + G_CALLBACK (gs_shell_reload_cb), shell); +} + +static void +initial_refresh_done (GsLoadingPage *loading_page, gpointer data) +{ + GsShell *shell = data; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + g_signal_handlers_disconnect_by_func (loading_page, initial_refresh_done, data); + + g_signal_emit (shell, signals[SIGNAL_LOADED], 0); + + /* if the "loaded" signal handler didn't change the mode, kick off async + * overview page refresh, and switch to the page once done */ + if (priv->mode == GS_SHELL_MODE_LOADING) { + GsPage *page; + + page = GS_PAGE (gtk_builder_get_object (priv->builder, "overview_page")); + g_signal_connect (page, "refreshed", + G_CALLBACK (overview_page_refresh_done), shell); + gs_page_reload (page); + return; + } + + /* now that we're finished with the loading page, connect the reload signal handler */ + g_signal_connect (priv->plugin_loader, "reload", + G_CALLBACK (gs_shell_reload_cb), shell); +} + +static gboolean +window_keypress_handler (GtkWidget *window, GdkEvent *event, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *w; + + /* handle ctrl+f shortcut */ + if (event->type == GDK_KEY_PRESS) { + GdkEventKey *e = (GdkEventKey *) event; + if ((e->state & GDK_CONTROL_MASK) > 0 && + e->keyval == GDK_KEY_f) { + w = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_bar")); + if (!gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (w))) { + gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (w), TRUE); + w = GTK_WIDGET (gtk_builder_get_object (priv->builder, + "entry_search")); + gtk_widget_grab_focus (w); + } else { + gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (w), FALSE); + } + return GDK_EVENT_STOP; + } + } + + /* pass to search bar */ + w = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_bar")); + return gtk_search_bar_handle_event (GTK_SEARCH_BAR (w), event); +} + +static void +search_changed_handler (GObject *entry, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const gchar *text; + + text = gtk_entry_get_text (GTK_ENTRY (entry)); + if (strlen (text) > 2) { + if (gs_shell_get_mode (shell) != GS_SHELL_MODE_SEARCH) { + save_back_entry (shell); + gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH, + (gpointer) text, TRUE); + } else { + GsPage *page = GS_PAGE (g_hash_table_lookup (priv->pages, "search")); + gs_search_page_set_text (GS_SEARCH_PAGE (page), text); + gs_page_switch_to (page, TRUE); + } + } +} + +static void +search_button_clicked_cb (GtkToggleButton *toggle_button, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *search_bar; + + search_bar = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_bar")); + gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (search_bar), + gtk_toggle_button_get_active (toggle_button)); + + if (priv->in_mode_change) + return; + + /* go back when exiting the search view */ + if (priv->mode == GS_SHELL_MODE_SEARCH && + !gtk_toggle_button_get_active (toggle_button)) + gs_shell_go_back (shell); +} + +static void +search_mode_enabled_cb (GtkSearchBar *search_bar, GParamSpec *pspec, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *search_button; + + search_button = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_button")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (search_button), + gtk_search_bar_get_search_mode (search_bar)); +} + +static gboolean +window_key_press_event (GtkWidget *win, GdkEventKey *event, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GdkKeymap *keymap; + GdkModifierType state; + gboolean is_rtl; + GtkWidget *button; + + button = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_back")); + if (!gtk_widget_is_visible (button) || !gtk_widget_is_sensitive (button)) + return GDK_EVENT_PROPAGATE; + + state = event->state; + keymap = gdk_keymap_get_for_display (gtk_widget_get_display (win)); + gdk_keymap_add_virtual_modifiers (keymap, &state); + state = state & gtk_accelerator_get_default_mod_mask (); + is_rtl = gtk_widget_get_direction (button) == GTK_TEXT_DIR_RTL; + + if ((!is_rtl && state == GDK_MOD1_MASK && event->keyval == GDK_KEY_Left) || + (is_rtl && state == GDK_MOD1_MASK && event->keyval == GDK_KEY_Right) || + event->keyval == GDK_KEY_Back) { + gtk_widget_activate (button); + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +window_button_press_event (GtkWidget *win, GdkEventButton *event, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *button; + + /* Mouse hardware back button is 8 */ + if (event->button != 8) + return GDK_EVENT_PROPAGATE; + + button = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_back")); + if (!gtk_widget_is_visible (button) || !gtk_widget_is_sensitive (button)) + return GDK_EVENT_PROPAGATE; + + gtk_widget_activate (button); + return GDK_EVENT_STOP; +} + +static gboolean +main_window_closed_cb (GtkWidget *dialog, GdkEvent *event, gpointer user_data) +{ + GsShell *shell = user_data; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *widget; + + /* hide any notifications */ + g_application_withdraw_notification (g_application_get_default (), + "installed"); + g_application_withdraw_notification (g_application_get_default (), + "install-resources"); + + /* clear any in-app notification */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event")); + gtk_revealer_set_reveal_child (GTK_REVEALER (widget), FALSE); + + /* release our hold on the download scheduler */ +#ifdef HAVE_MOGWAI + if (priv->scheduler != NULL) { + if (priv->scheduler_invalidated_handler > 0) + g_signal_handler_disconnect (priv->scheduler, + priv->scheduler_invalidated_handler); + priv->scheduler_invalidated_handler = 0; + + mwsc_scheduler_release_async (priv->scheduler, + NULL, + scheduler_release_cb, + g_object_ref (shell)); + } +#endif /* HAVE_MOGWAI */ + + gs_shell_clean_back_entry_stack (shell); + gtk_widget_hide (dialog); + return TRUE; +} + +static void +gs_shell_main_window_mapped_cb (GtkWidget *widget, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + gs_plugin_loader_set_scale (priv->plugin_loader, + (guint) gtk_widget_get_scale_factor (widget)); + + /* Set up the updates bar. Do this here rather than in gs_shell_setup() + * since we only want to hold the scheduler open while the gnome-software + * main window is visible, and not while we’re running in the background. */ +#ifdef HAVE_MOGWAI + if (priv->scheduler == NULL) + mwsc_scheduler_new_async (priv->cancellable, + (GAsyncReadyCallback) scheduler_ready_cb, + g_object_ref (shell)); +#else + gs_shell_refresh_auto_updates_ui (shell); +#endif /* HAVE_MOGWAI */ +} + +static void +gs_shell_main_window_realized_cb (GtkWidget *widget, GsShell *shell) +{ + + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GdkRectangle geometry; + GdkDisplay *display; + GdkMonitor *monitor; + + display = gtk_widget_get_display (GTK_WIDGET (priv->main_window)); + monitor = gdk_display_get_monitor_at_window (display, + gtk_widget_get_window (GTK_WIDGET (priv->main_window))); + + /* adapt the window for low and medium resolution screens */ + gdk_monitor_get_geometry (monitor, &geometry); + if (geometry.width < 800 || geometry.height < 600) { + GtkWidget *buttonbox = GTK_WIDGET (gtk_builder_get_object (priv->builder, "buttonbox_main")); + + gtk_container_child_set (GTK_CONTAINER (buttonbox), + GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_explore")), + "non-homogeneous", TRUE, + NULL); + gtk_container_child_set (GTK_CONTAINER (buttonbox), + GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_installed")), + "non-homogeneous", TRUE, + NULL); + gtk_container_child_set (GTK_CONTAINER (buttonbox), + GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_updates")), + "non-homogeneous", TRUE, + NULL); + } else if (geometry.width < 1366 || geometry.height < 768) { + gtk_window_set_default_size (priv->main_window, 1050, 600); + } +} + +static void +gs_shell_allow_updates_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *widget; + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_updates")); + gtk_widget_set_visible (widget, gs_plugin_loader_get_allow_updates (plugin_loader) || + priv->mode == GS_SHELL_MODE_UPDATES); +} + +typedef enum { + GS_SHELL_EVENT_BUTTON_NONE = 0, + GS_SHELL_EVENT_BUTTON_SOURCES = 1 << 0, + GS_SHELL_EVENT_BUTTON_NO_SPACE = 1 << 1, + GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS = 1 << 2, + GS_SHELL_EVENT_BUTTON_MORE_INFO = 1 << 3, + GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED = 1 << 4, + GS_SHELL_EVENT_BUTTON_LAST +} GsShellEventButtons; + +static gboolean +gs_shell_has_disk_examination_app (void) +{ + g_autofree gchar *baobab = g_find_program_in_path ("baobab"); + return (baobab != NULL); +} + +static void +gs_shell_show_event_app_notify (GsShell *shell, + const gchar *title, + GsShellEventButtons buttons) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *widget; + + /* set visible */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event")); + gtk_revealer_set_reveal_child (GTK_REVEALER (widget), TRUE); + + /* sources button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_sources")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_SOURCES) > 0); + + /* no-space button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_no_space")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_NO_SPACE) > 0 && + gs_shell_has_disk_examination_app()); + + /* network settings button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_network_settings")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS) > 0); + + /* restart button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_restart_required")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED) > 0); + + /* more-info button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_more_info")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_MORE_INFO) > 0); + + /* dismiss button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_dismiss")); + gtk_widget_set_visible (widget, (buttons & GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED) == 0); + + /* set title */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_events")); + gtk_label_set_markup (GTK_LABEL (widget), title); + gtk_widget_set_visible (widget, title != NULL); +} + +void +gs_shell_show_notification (GsShell *shell, const gchar *title) +{ + gs_shell_show_event_app_notify (shell, title, GS_SHELL_EVENT_BUTTON_NONE); +} + +static gchar * +gs_shell_get_title_from_origin (GsApp *app) +{ + /* get a title, falling back */ + if (gs_app_get_origin_hostname (app) != NULL) { + /* TRANSLATORS: this is part of the in-app notification, + * where the %s is the truncated hostname, e.g. + * 'alt.fedoraproject.org' */ + return g_strdup_printf (_("“%s”"), gs_app_get_origin_hostname (app)); + } + if (gs_app_get_origin (app) != NULL) { + /* TRANSLATORS: this is part of the in-app notification, + * where the %s is the origin id, e.g. 'fedora' */ + return g_strdup_printf (_("“%s”"), gs_app_get_origin (app)); + } + return g_strdup_printf ("“%s”", gs_app_get_id (app)); +} + +/* return a name for the app, using quotes if the name is more than one word */ +static gchar * +gs_shell_get_title_from_app (GsApp *app) +{ + const gchar *tmp = gs_app_get_name (app); + if (tmp != NULL) { + if (g_strstr_len (tmp, -1, " ") != NULL) { + /* TRANSLATORS: this is part of the in-app notification, + * where the %s is a multi-word localised app name + * e.g. 'Getting things GNOME!" */ + return g_strdup_printf (_("“%s”"), tmp); + } + return g_strdup (tmp); + } + return g_strdup_printf (_("“%s”"), gs_app_get_id (app)); +} + +static gchar * +get_first_line (const gchar *str) +{ + g_auto(GStrv) lines = NULL; + + lines = g_strsplit (str, "\n", 2); + if (lines != NULL && g_strv_length (lines) != 0) + return g_strdup (lines[0]); + + return NULL; +} + +static void +gs_shell_append_detailed_error (GsShell *shell, GString *str, const GError *error) +{ + g_autofree gchar *first_line = get_first_line (error->message); + if (first_line != NULL) { + g_autofree gchar *escaped = g_markup_escape_text (first_line, -1); + g_string_append_printf (str, ":\n%s", escaped); + } +} + +static gboolean +gs_shell_show_event_refresh (GsShell *shell, GsPluginEvent *event) +{ + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + GsPluginAction action = gs_plugin_event_get_action (event); + g_autofree gchar *str_origin = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + /* ignore any errors from background downloads */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + + switch (error->code) { + case GS_PLUGIN_ERROR_DOWNLOAD_FAILED: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + if (gs_app_get_bundle_kind (origin) == AS_BUNDLE_KIND_CABINET) { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the source (e.g. "alt.fedoraproject.org") */ + g_string_append_printf (str, _("Unable to download " + "firmware updates from %s"), + str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the source (e.g. "alt.fedoraproject.org") */ + g_string_append_printf (str, _("Unable to download updates from %s"), + str_origin); + if (gs_app_get_management_plugin (origin) != NULL) + buttons |= GS_SHELL_EVENT_BUTTON_SOURCES; + } + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates")); + } + gs_shell_append_detailed_error (shell, str, error); + break; + case GS_PLUGIN_ERROR_NO_NETWORK: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates: " + "internet access was required but wasn’t available")); + buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS; + break; + case GS_PLUGIN_ERROR_NO_SPACE: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the source (e.g. "alt.fedoraproject.org") */ + g_string_append_printf (str, _("Unable to download updates " + "from %s: not enough disk space"), + str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates: " + "not enough disk space")); + } + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_AUTH_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates: " + "authentication was required")); + break; + case GS_PLUGIN_ERROR_AUTH_INVALID: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates: " + "authentication was invalid")); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates: you do not have" + " permission to install software")); + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + if (action == GS_PLUGIN_ACTION_DOWNLOAD) { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to download updates")); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to get list of updates")); + } + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_install (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autofree gchar *str_app = NULL; + g_autofree gchar *str_origin = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + str_app = gs_shell_get_title_from_app (app); + switch (error->code) { + case GS_PLUGIN_ERROR_DOWNLOAD_FAILED: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the first %s is the application name (e.g. "GIMP") and + * the second %s is the origin, e.g. "Fedora Project [fedoraproject.org]" */ + g_string_append_printf (str, _("Unable to install %s as " + "download failed from %s"), + str_app, str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s " + "as download failed"), + str_app); + } + gs_shell_append_detailed_error (shell, str, error); + break; + case GS_PLUGIN_ERROR_NOT_SUPPORTED: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the first %s is the application name (e.g. "GIMP") + * and the second %s is the name of the runtime, e.g. + * "GNOME SDK [flatpak.gnome.org]" */ + g_string_append_printf (str, _("Unable to install %s as " + "runtime %s not available"), + str_app, str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s " + "as not supported"), + str_app); + } + break; + case GS_PLUGIN_ERROR_NO_NETWORK: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to install: internet access was " + "required but wasn’t available")); + buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS; + break; + case GS_PLUGIN_ERROR_INVALID_FORMAT: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to install: the application has an invalid format")); + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s: " + "not enough disk space"), + str_app); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_AUTH_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install %s: " + "authentication was required"), + str_app); + break; + case GS_PLUGIN_ERROR_AUTH_INVALID: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s: " + "authentication was invalid"), + str_app); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s: " + "you do not have permission to " + "install software"), + str_app); + break; + case GS_PLUGIN_ERROR_AC_POWER_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to install %s: " + "AC power is required"), + str_app); + break; + case GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to install %s: " + "The battery level is too low"), + str_app); + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to install %s"), str_app); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_update (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autofree gchar *str_app = NULL; + g_autofree gchar *str_origin = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + /* ignore any errors from background downloads */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + + switch (error->code) { + case GS_PLUGIN_ERROR_DOWNLOAD_FAILED: + if (app != NULL && origin != NULL) { + str_app = gs_shell_get_title_from_app (app); + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the first %s is the app name (e.g. "GIMP") and + * the second %s is the origin, e.g. "Fedora" or + * "Fedora Project [fedoraproject.org]" */ + g_string_append_printf (str, _("Unable to update %s from %s as download failed"), + str_app, str_origin); + buttons = TRUE; + } else if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s as download failed"), + str_app); + } else if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the origin, e.g. "Fedora" or + * "Fedora Project [fedoraproject.org]" */ + g_string_append_printf (str, _("Unable to install updates from %s as download failed"), + str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates as download failed")); + } + gs_shell_append_detailed_error (shell, str, error); + break; + case GS_PLUGIN_ERROR_NO_NETWORK: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Unable to update: " + "internet access was required but " + "wasn’t available")); + buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS; + break; + case GS_PLUGIN_ERROR_NO_SPACE: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s: " + "not enough disk space"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates: " + "not enough disk space")); + } + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_AUTH_REQUIRED: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s: " + "authentication was required"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates: " + "authentication was required")); + } + break; + case GS_PLUGIN_ERROR_AUTH_INVALID: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s: " + "authentication was invalid"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates: " + "authentication was invalid")); + } + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s: " + "you do not have permission to " + "update software"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates: " + "you do not have permission to " + "update software")); + } + break; + case GS_PLUGIN_ERROR_AC_POWER_REQUIRED: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to update %s: " + "AC power is required"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to install updates: " + "AC power is required")); + } + break; + case GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to update %s: " + "The battery level is too low"), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "Dell XPS 13") */ + g_string_append_printf (str, _("Unable to install updates: " + "The battery level is too low")); + } + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to update %s"), str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append_printf (str, _("Unable to install updates")); + } + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_upgrade (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *str_app = NULL; + g_autofree gchar *str_origin = NULL; + + str_app = g_strdup_printf ("%s %s", gs_app_get_name (app), gs_app_get_version (app)); + switch (error->code) { + case GS_PLUGIN_ERROR_DOWNLOAD_FAILED: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the first %s is the distro name (e.g. "Fedora 25") and + * the second %s is the origin, e.g. "Fedora Project [fedoraproject.org]" */ + g_string_append_printf (str, _("Unable to upgrade to %s from %s"), + str_app, str_origin); + } else { + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the app name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to upgrade to %s " + "as download failed"), + str_app); + } + gs_shell_append_detailed_error (shell, str, error); + break; + case GS_PLUGIN_ERROR_NO_NETWORK: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "internet access was required but " + "wasn’t available"), + str_app); + buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS; + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "not enough disk space"), + str_app); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_AUTH_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "authentication was required"), + str_app); + break; + case GS_PLUGIN_ERROR_AUTH_INVALID: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "authentication was invalid"), + str_app); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "you do not have permission to upgrade"), + str_app); + break; + case GS_PLUGIN_ERROR_AC_POWER_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "AC power is required"), + str_app); + break; + case GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s: " + "The battery level is too low"), + str_app); + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the distro name (e.g. "Fedora 25") */ + g_string_append_printf (str, _("Unable to upgrade to %s"), str_app); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_remove (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *str_app = NULL; + + str_app = gs_shell_get_title_from_app (app); + switch (error->code) { + case GS_PLUGIN_ERROR_AUTH_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s: authentication was required"), + str_app); + break; + case GS_PLUGIN_ERROR_AUTH_INVALID: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s: authentication was invalid"), + str_app); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s: you do not have" + " permission to remove software"), + str_app); + break; + case GS_PLUGIN_ERROR_AC_POWER_REQUIRED: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s: " + "AC power is required"), + str_app); + break; + case GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s: " + "The battery level is too low"), + str_app); + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* non-interactive generic */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("Unable to remove %s"), str_app); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_launch (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *str_app = NULL; + g_autofree gchar *str_origin = NULL; + + switch (error->code) { + case GS_PLUGIN_ERROR_NOT_SUPPORTED: + if (app != NULL && origin != NULL) { + str_app = gs_shell_get_title_from_app (app); + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * where the first %s is the application name (e.g. "GIMP") + * and the second %s is the name of the runtime, e.g. + * "GNOME SDK [flatpak.gnome.org]" */ + g_string_append_printf (str, _("Unable to launch %s: %s is not installed"), + str_app, + str_origin); + } + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Not enough disk space — free up some space " + "and try again")); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* non-interactive generic */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + /* TRANSLATORS: we failed to get a proper error code */ + g_string_append (str, _("Sorry, something went wrong")); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_file_to_app (GsShell *shell, GsPluginEvent *event) +{ + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + + switch (error->code) { + case GS_PLUGIN_ERROR_NOT_SUPPORTED: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Failed to install file: not supported")); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Failed to install file: authentication failed")); + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Not enough disk space — free up some space " + "and try again")); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* non-interactive generic */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + /* TRANSLATORS: we failed to get a proper error code */ + g_string_append (str, _("Sorry, something went wrong")); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_url_to_app (GsShell *shell, GsPluginEvent *event) +{ + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + + switch (error->code) { + case GS_PLUGIN_ERROR_NOT_SUPPORTED: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Failed to install: not supported")); + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Failed to install: authentication failed")); + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Not enough disk space — free up some space " + "and try again")); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* non-interactive generic */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + /* TRANSLATORS: we failed to get a proper error code */ + g_string_append (str, _("Sorry, something went wrong")); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event_fallback (GsShell *shell, GsPluginEvent *event) +{ + GsApp *app = gs_plugin_event_get_app (event); + GsApp *origin = gs_plugin_event_get_origin (event); + GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + const GError *error = gs_plugin_event_get_error (event); + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *str_app = NULL; + g_autofree gchar *str_origin = NULL; + + switch (error->code) { + case GS_PLUGIN_ERROR_DOWNLOAD_FAILED: + if (origin != NULL) { + str_origin = gs_shell_get_title_from_origin (origin); + /* TRANSLATORS: failure text for the in-app notification, + * the %s is the origin, e.g. "Fedora" or + * "Fedora Project [fedoraproject.org]" */ + g_string_append_printf (str, _("Unable to contact %s"), + str_origin); + } + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("Not enough disk space — free up some space " + "and try again")); + buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE; + break; + case GS_PLUGIN_ERROR_RESTART_REQUIRED: + if (app != NULL) { + str_app = gs_shell_get_title_from_app (app); + /* TRANSLATORS: failure text for the in-app notification, + * where the %s is the application name (e.g. "GIMP") */ + g_string_append_printf (str, _("%s needs to be restarted " + "to use new plugins."), + str_app); + } else { + /* TRANSLATORS: failure text for the in-app notification */ + g_string_append (str, _("This application needs to be " + "restarted to use new plugins.")); + } + buttons |= GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED; + break; + case GS_PLUGIN_ERROR_AC_POWER_REQUIRED: + /* TRANSLATORS: need to be connected to the AC power */ + g_string_append (str, _("AC power is required")); + break; + case GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: + /* TRANSLATORS: not enough juice to do this safely */ + g_string_append (str, _("The battery level is too low")); + break; + case GS_PLUGIN_ERROR_CANCELLED: + break; + default: + /* non-interactive generic */ + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE)) + return FALSE; + /* TRANSLATORS: we failed to get a proper error code */ + g_string_append (str, _("Sorry, something went wrong")); + gs_shell_append_detailed_error (shell, str, error); + break; + } + if (str->len == 0) + return FALSE; + + /* add more-info button */ + if (origin != NULL) { + const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP); + if (uri != NULL) { + g_free (priv->events_info_uri); + priv->events_info_uri = g_strdup (uri); + buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO; + } + } + + /* show in-app notification */ + gs_shell_show_event_app_notify (shell, str->str, buttons); + return TRUE; +} + +static gboolean +gs_shell_show_event (GsShell *shell, GsPluginEvent *event) +{ + const GError *error; + GsPluginAction action; + + /* get error */ + error = gs_plugin_event_get_error (event); + if (error == NULL) + return FALSE; + + /* name and shame the plugin */ + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT)) { + gs_shell_show_event_app_notify (shell, error->message, + GS_SHELL_EVENT_BUTTON_NONE); + return TRUE; + } + + /* split up the events by action */ + action = gs_plugin_event_get_action (event); + switch (action) { + case GS_PLUGIN_ACTION_REFRESH: + case GS_PLUGIN_ACTION_DOWNLOAD: + return gs_shell_show_event_refresh (shell, event); + case GS_PLUGIN_ACTION_INSTALL: + return gs_shell_show_event_install (shell, event); + case GS_PLUGIN_ACTION_UPDATE: + return gs_shell_show_event_update (shell, event); + case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: + return gs_shell_show_event_upgrade (shell, event); + case GS_PLUGIN_ACTION_REMOVE: + return gs_shell_show_event_remove (shell, event); + case GS_PLUGIN_ACTION_LAUNCH: + return gs_shell_show_event_launch (shell, event); + case GS_PLUGIN_ACTION_FILE_TO_APP: + return gs_shell_show_event_file_to_app (shell, event); + case GS_PLUGIN_ACTION_URL_TO_APP: + return gs_shell_show_event_url_to_app (shell, event); + default: + break; + } + + /* capture some warnings every time */ + return gs_shell_show_event_fallback (shell, event); +} + +static void +gs_shell_rescan_events (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *widget; + g_autoptr(GsPluginEvent) event = NULL; + + /* find the first active event and show it */ + event = gs_plugin_loader_get_event_default (priv->plugin_loader); + if (event != NULL) { + if (!gs_shell_show_event (shell, event)) { + GsPluginAction action = gs_plugin_event_get_action (event); + const GError *error = gs_plugin_event_get_error (event); + if (error != NULL && + !g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED) && + !g_error_matches (error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED)) { + g_warning ("not handling error %s for action %s: %s", + gs_plugin_error_to_string (error->code), + gs_plugin_action_to_string (action), + error->message); + } + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID); + return; + } + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE); + return; + } + + /* nothing to show */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "notification_event")); + gtk_revealer_set_reveal_child (GTK_REVEALER (widget), FALSE); +} + +static void +gs_shell_events_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsShell *shell) +{ + gs_shell_rescan_events (shell); +} + +static void +gs_shell_plugin_event_dismissed_cb (GtkButton *button, GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + guint i; + g_autoptr(GPtrArray) events = NULL; + + /* mark any events currently showing as invalid */ + events = gs_plugin_loader_get_events (priv->plugin_loader); + for (i = 0; i < events->len; i++) { + GsPluginEvent *event = g_ptr_array_index (events, i); + if (gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE)) { + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID); + gs_plugin_event_remove_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE); + } + } + + /* show the next event */ + gs_shell_rescan_events (shell); +} + +static void +gs_shell_setup_pages (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_autoptr(GList) keys = g_hash_table_get_keys (priv->pages); + for (GList *l = keys; l != NULL; l = l->next) { + g_autoptr(GError) error = NULL; + GsPage *page = GS_PAGE (g_hash_table_lookup (priv->pages, l->data)); + if (!gs_page_setup (page, shell, + priv->plugin_loader, + priv->builder, + priv->cancellable, + &error)) { + g_warning ("Failed to setup panel: %s", error->message); + } + } +} + +static void +gs_shell_add_about_menu_item (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GMenu *primary_menu; + g_autoptr(GMenuItem) menu_item = NULL; + g_autofree gchar *label = NULL; + + primary_menu = G_MENU (gtk_builder_get_object (priv->builder, "primary_menu")); + + /* TRANSLATORS: this is the menu item that opens the about window */ + menu_item = g_menu_item_new (_("About Software"), "app.about"); + g_menu_append_item (primary_menu, menu_item); +} + +void +gs_shell_setup (GsShell *shell, GsPluginLoader *plugin_loader, GCancellable *cancellable) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *widget; + GtkStyleContext *style_context; + GsPage *page; + + g_return_if_fail (GS_IS_SHELL (shell)); + + priv->plugin_loader = g_object_ref (plugin_loader); + g_signal_connect_object (priv->plugin_loader, "notify::events", + G_CALLBACK (gs_shell_events_notify_cb), + shell, 0); + g_signal_connect_object (priv->plugin_loader, "notify::allow-updates", + G_CALLBACK (gs_shell_allow_updates_notify_cb), + shell, 0); + g_signal_connect_object (priv->plugin_loader, "notify::network-metered", + G_CALLBACK (gs_shell_network_metered_notify_cb), + shell, 0); + g_signal_connect_object (priv->plugin_loader, "basic-auth-start", + G_CALLBACK (gs_shell_basic_auth_start_cb), + shell, 0); + priv->cancellable = g_object_ref (cancellable); + + priv->settings = g_settings_new ("org.gnome.software"); + + /* get UI */ + priv->builder = gtk_builder_new_from_resource ("/org/gnome/Software/gnome-software.ui"); + priv->main_window = GTK_WINDOW (gtk_builder_get_object (priv->builder, "window_software")); + g_signal_connect (priv->main_window, "map", + G_CALLBACK (gs_shell_main_window_mapped_cb), shell); + g_signal_connect (priv->main_window, "realize", + G_CALLBACK (gs_shell_main_window_realized_cb), shell); + + g_signal_connect (priv->main_window, "delete-event", + G_CALLBACK (main_window_closed_cb), shell); + + /* fix up the header bar */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "header")); + if (gs_utils_is_current_desktop ("Unity")) { + style_context = gtk_widget_get_style_context (widget); + gtk_style_context_remove_class (style_context, GTK_STYLE_CLASS_TITLEBAR); + gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_PRIMARY_TOOLBAR); + gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (widget), ""); + } else { + g_object_ref (widget); + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (widget)), widget); + gtk_window_set_titlebar (GTK_WINDOW (priv->main_window), widget); + g_object_unref (widget); + } + + /* global keynav */ + g_signal_connect_after (priv->main_window, "key_press_event", + G_CALLBACK (window_key_press_event), shell); + /* mouse hardware back button */ + g_signal_connect_after (priv->main_window, "button_press_event", + G_CALLBACK (window_button_press_event), shell); + + /* show the search bar when clicking on the search button */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_button")); + g_signal_connect (widget, "clicked", + G_CALLBACK (search_button_clicked_cb), + shell); + /* set the search button enabled when search bar appears */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "search_bar")); + g_signal_connect (widget, "notify::search-mode-enabled", + G_CALLBACK (search_mode_enabled_cb), + shell); + + /* show the account popover when clicking on the account button */ + /* widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "menu_button")); + g_signal_connect (widget, "clicked", + G_CALLBACK (menu_button_clicked_cb), + shell); */ + + /* setup buttons */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_back")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_back_button_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_explore")); + g_object_set_data (G_OBJECT (widget), + "gnome-software::overview-mode", + GINT_TO_POINTER (GS_SHELL_MODE_OVERVIEW)); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_overview_page_button_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_installed")); + g_object_set_data (G_OBJECT (widget), + "gnome-software::overview-mode", + GINT_TO_POINTER (GS_SHELL_MODE_INSTALLED)); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_overview_page_button_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_updates")); + g_object_set_data (G_OBJECT (widget), + "gnome-software::overview-mode", + GINT_TO_POINTER (GS_SHELL_MODE_UPDATES)); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_overview_page_button_cb), shell); + + /* set up in-app notification controls */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_dismiss")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_event_dismissed_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_sources")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_events_sources_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_no_space")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_events_no_space_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_network_settings")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_events_network_settings_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_more_info")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_events_more_info_cb), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "button_events_restart_required")); + g_signal_connect (widget, "clicked", + G_CALLBACK (gs_shell_plugin_events_restart_required_cb), shell); + + /* add pages to hash */ + page = GS_PAGE (gtk_builder_get_object (priv->builder, "overview_page")); + g_hash_table_insert (priv->pages, g_strdup ("overview"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "updates_page")); + g_hash_table_insert (priv->pages, g_strdup ("updates"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "installed_page")); + g_hash_table_insert (priv->pages, g_strdup ("installed"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "moderate_page")); + g_hash_table_insert (priv->pages, g_strdup ("moderate"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "loading_page")); + g_hash_table_insert (priv->pages, g_strdup ("loading"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "search_page")); + g_hash_table_insert (priv->pages, g_strdup ("search"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "details_page")); + g_hash_table_insert (priv->pages, g_strdup ("details"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "category_page")); + g_hash_table_insert (priv->pages, g_strdup ("category"), page); + page = GS_PAGE (gtk_builder_get_object (priv->builder, "extras_page")); + g_hash_table_insert (priv->pages, g_strdup ("extras"), page); + gs_shell_setup_pages (shell); + + /* set up the metered data info bar and mogwai */ + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "metered_updates_bar")); + g_signal_connect (widget, "response", + (GCallback) gs_shell_metered_updates_bar_response_cb, shell); + + g_signal_connect (priv->settings, "changed::download-updates", + (GCallback) gs_shell_download_updates_changed_cb, shell); + + /* set up search */ + g_signal_connect (priv->main_window, "key-press-event", + G_CALLBACK (window_keypress_handler), shell); + widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "entry_search")); + priv->search_changed_id = + g_signal_connect (widget, "search-changed", + G_CALLBACK (search_changed_handler), shell); + + /* load content */ + page = GS_PAGE (gtk_builder_get_object (priv->builder, "loading_page")); + g_signal_connect (page, "refreshed", + G_CALLBACK (initial_refresh_done), shell); + + /* coldplug */ + gs_shell_rescan_events (shell); + + /* primary menu */ + gs_shell_add_about_menu_item (shell); + + /* show loading page, which triggers the initial refresh */ + gs_shell_change_mode (shell, GS_SHELL_MODE_LOADING, NULL, TRUE); +} + +void +gs_shell_reset_state (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + /* reset to overview, unless we're in the loading state which advances + * to overview on its own */ + if (priv->mode != GS_SHELL_MODE_LOADING) + priv->mode = GS_SHELL_MODE_OVERVIEW; + + gs_shell_clean_back_entry_stack (shell); +} + +void +gs_shell_set_mode (GsShell *shell, GsShellMode mode) +{ + gs_shell_change_mode (shell, mode, NULL, TRUE); +} + +GsShellMode +gs_shell_get_mode (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + return priv->mode; +} + +const gchar * +gs_shell_get_mode_string (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + return page_name[priv->mode]; +} + +void +gs_shell_install (GsShell *shell, GsApp *app, GsShellInteraction interaction) +{ + GsPage *page; + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + save_back_entry (shell); + gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS, + (gpointer) app, TRUE); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "details")); + gs_page_install_app (page, app, interaction, priv->cancellable); +} + +void +gs_shell_show_installed_updates (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *dialog; + + dialog = gs_update_dialog_new (priv->plugin_loader); + gs_update_dialog_show_installed_updates (GS_UPDATE_DIALOG (dialog)); + + gtk_window_set_transient_for (GTK_WINDOW (dialog), priv->main_window); + gtk_window_present (GTK_WINDOW (dialog)); +} + +void +gs_shell_show_sources (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *dialog; + + /* use if available */ + if (g_spawn_command_line_async ("software-properties-gtk", NULL)) + return; + + dialog = gs_repos_dialog_new (priv->main_window, priv->plugin_loader); + gs_shell_modal_dialog_present (shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +void +gs_shell_show_prefs (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GtkWidget *dialog; + + dialog = gs_prefs_dialog_new (priv->main_window, priv->plugin_loader); + gs_shell_modal_dialog_present (shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +void +gs_shell_show_app (GsShell *shell, GsApp *app) +{ + save_back_entry (shell); + gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS, app, TRUE); + gs_shell_activate (shell); +} + +void +gs_shell_show_category (GsShell *shell, GsCategory *category) +{ + save_back_entry (shell); + gs_shell_change_mode (shell, GS_SHELL_MODE_CATEGORY, category, TRUE); +} + +void gs_shell_show_extras_search (GsShell *shell, const gchar *mode, gchar **resources) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GsPage *page; + + page = GS_PAGE (gtk_builder_get_object (priv->builder, "extras_page")); + + save_back_entry (shell); + gs_extras_page_search (GS_EXTRAS_PAGE (page), mode, resources); + gs_shell_change_mode (shell, GS_SHELL_MODE_EXTRAS, NULL, TRUE); + gs_shell_activate (shell); +} + +void +gs_shell_show_search (GsShell *shell, const gchar *search) +{ + save_back_entry (shell); + gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH, + (gpointer) search, TRUE); +} + +void +gs_shell_show_local_file (GsShell *shell, GFile *file) +{ + g_autoptr(GsApp) app = gs_app_new (NULL); + save_back_entry (shell); + gs_app_set_local_file (app, file); + gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS, + (gpointer) app, TRUE); + gs_shell_activate (shell); +} + +void +gs_shell_show_search_result (GsShell *shell, const gchar *id, const gchar *search) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + GsPage *page; + + save_back_entry (shell); + page = GS_PAGE (g_hash_table_lookup (priv->pages, "search")); + gs_search_page_set_appid_to_show (GS_SEARCH_PAGE (page), id); + gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH, + (gpointer) search, TRUE); +} + +void +gs_shell_show_uri (GsShell *shell, const gchar *url) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + g_autoptr(GError) error = NULL; + + if (!gtk_show_uri_on_window (priv->main_window, + url, + GDK_CURRENT_TIME, + &error)) { + g_warning ("failed to show URI %s: %s", + url, error->message); + } +} + +static void +gs_shell_dispose (GObject *object) +{ + GsShell *shell = GS_SHELL (object); + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + if (priv->back_entry_stack != NULL) { + g_queue_free_full (priv->back_entry_stack, (GDestroyNotify) free_back_entry); + priv->back_entry_stack = NULL; + } + g_clear_object (&priv->builder); + g_clear_object (&priv->cancellable); + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->header_start_widget); + g_clear_object (&priv->header_end_widget); + g_clear_object (&priv->page); + g_clear_pointer (&priv->pages, g_hash_table_unref); + g_clear_pointer (&priv->events_info_uri, g_free); + g_clear_pointer (&priv->modal_dialogs, g_ptr_array_unref); + g_clear_object (&priv->settings); + +#ifdef HAVE_MOGWAI + if (priv->scheduler != NULL) { + if (priv->scheduler_invalidated_handler > 0) + g_signal_handler_disconnect (priv->scheduler, + priv->scheduler_invalidated_handler); + + mwsc_scheduler_release_async (priv->scheduler, + NULL, + scheduler_release_cb, + g_object_ref (shell)); + } +#endif /* HAVE_MOGWAI */ + + G_OBJECT_CLASS (gs_shell_parent_class)->dispose (object); +} + +static void +gs_shell_class_init (GsShellClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_shell_dispose; + + signals [SIGNAL_LOADED] = + g_signal_new ("loaded", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsShellClass, loaded), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +gs_shell_init (GsShell *shell) +{ + GsShellPrivate *priv = gs_shell_get_instance_private (shell); + + priv->pages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + priv->back_entry_stack = g_queue_new (); + priv->ignore_primary_buttons = FALSE; + priv->modal_dialogs = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_widget_destroy); +} + +GsShell * +gs_shell_new (void) +{ + GsShell *shell; + shell = g_object_new (GS_TYPE_SHELL, NULL); + return GS_SHELL (shell); +} diff --git a/src/gs-shell.h b/src/gs-shell.h new file mode 100644 index 0000000..a02d24d --- /dev/null +++ b/src/gs-shell.h @@ -0,0 +1,95 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_SHELL (gs_shell_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsShell, gs_shell, GS, SHELL, GObject) + +struct _GsShellClass +{ + GObjectClass parent_class; + + void (* loaded) (GsShell *shell); +}; + +typedef enum { + GS_SHELL_MODE_UNKNOWN, + GS_SHELL_MODE_OVERVIEW, + GS_SHELL_MODE_INSTALLED, + GS_SHELL_MODE_SEARCH, + GS_SHELL_MODE_UPDATES, + GS_SHELL_MODE_DETAILS, + GS_SHELL_MODE_CATEGORY, + GS_SHELL_MODE_EXTRAS, + GS_SHELL_MODE_MODERATE, + GS_SHELL_MODE_LOADING, + GS_SHELL_MODE_LAST +} GsShellMode; + +typedef enum { + GS_SHELL_INTERACTION_NONE = (0u), + GS_SHELL_INTERACTION_NOTIFY = (1u << 0), + GS_SHELL_INTERACTION_FULL = (1u << 1) | GS_SHELL_INTERACTION_NOTIFY, + GS_SHELL_INTERACTION_LAST +} GsShellInteraction; + +GsShell *gs_shell_new (void); +void gs_shell_activate (GsShell *shell); +void gs_shell_refresh (GsShell *shell, + GCancellable *cancellable); +void gs_shell_change_mode (GsShell *shell, + GsShellMode mode, + gpointer data, + gboolean scroll_up); +void gs_shell_reset_state (GsShell *shell); +void gs_shell_set_mode (GsShell *shell, + GsShellMode mode); +void gs_shell_modal_dialog_present (GsShell *shell, + GtkDialog *dialog); +GsShellMode gs_shell_get_mode (GsShell *shell); +const gchar *gs_shell_get_mode_string (GsShell *shell); +void gs_shell_install (GsShell *shell, + GsApp *app, + GsShellInteraction interaction); +void gs_shell_show_installed_updates(GsShell *shell); +void gs_shell_show_sources (GsShell *shell); +void gs_shell_show_prefs (GsShell *shell); +void gs_shell_show_app (GsShell *shell, + GsApp *app); +void gs_shell_show_category (GsShell *shell, + GsCategory *category); +void gs_shell_show_search (GsShell *shell, + const gchar *search); +void gs_shell_show_local_file (GsShell *shell, + GFile *file); +void gs_shell_show_search_result (GsShell *shell, + const gchar *id, + const gchar *search); +void gs_shell_show_extras_search (GsShell *shell, + const gchar *mode, + gchar **resources); +void gs_shell_show_uri (GsShell *shell, + const gchar *url); +void gs_shell_setup (GsShell *shell, + GsPluginLoader *plugin_loader, + GCancellable *cancellable); +gboolean gs_shell_is_active (GsShell *shell); +GtkWindow *gs_shell_get_window (GsShell *shell); +void gs_shell_show_notification (GsShell *shell, + const gchar *title); + +G_END_DECLS diff --git a/src/gs-star-widget.c b/src/gs-star-widget.c new file mode 100644 index 0000000..4485af1 --- /dev/null +++ b/src/gs-star-widget.c @@ -0,0 +1,327 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <math.h> + +#include "gs-common.h" +#include "gs-star-widget.h" + +typedef struct +{ + gboolean interactive; + gint rating; + guint icon_size; + GtkWidget *box1; + GtkImage *images[5]; +} GsStarWidgetPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsStarWidget, gs_star_widget, GTK_TYPE_BIN) + +typedef enum { + PROP_ICON_SIZE = 1, + PROP_INTERACTIVE, + PROP_RATING, +} GsStarWidgetProperty; + +enum { + RATING_CHANGED, + SIGNAL_LAST +}; + +static GParamSpec *properties[PROP_RATING + 1] = { 0, }; +static guint signals [SIGNAL_LAST] = { 0 }; + +const gint rate_to_star[] = {20, 40, 60, 80, 100, -1}; + +static void gs_star_widget_refresh (GsStarWidget *star); + +gint +gs_star_widget_get_rating (GsStarWidget *star) +{ + GsStarWidgetPrivate *priv; + g_return_val_if_fail (GS_IS_STAR_WIDGET (star), -1); + priv = gs_star_widget_get_instance_private (star); + return priv->rating; +} + +void +gs_star_widget_set_icon_size (GsStarWidget *star, guint pixel_size) +{ + GsStarWidgetPrivate *priv; + g_return_if_fail (GS_IS_STAR_WIDGET (star)); + priv = gs_star_widget_get_instance_private (star); + + if (priv->icon_size == pixel_size) + return; + + priv->icon_size = pixel_size; + g_object_notify_by_pspec (G_OBJECT (star), properties[PROP_ICON_SIZE]); + gs_star_widget_refresh (star); +} + +static void +gs_star_widget_button_clicked_cb (GtkButton *button, GsStarWidget *star) +{ + GsStarWidgetPrivate *priv; + gint rating; + + priv = gs_star_widget_get_instance_private (star); + if (!priv->interactive) + return; + + rating = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), + "GsStarWidget::value")); + gs_star_widget_set_rating (star, rating); + g_signal_emit (star, signals[RATING_CHANGED], 0, priv->rating); +} + +/* Update the star styles to display the new rating */ +static void +gs_star_widget_refresh_rating (GsStarWidget *star) +{ + GsStarWidgetPrivate *priv = gs_star_widget_get_instance_private (star); + + if (!gtk_widget_get_realized (GTK_WIDGET (star))) + return; + + for (guint i = 0; i < G_N_ELEMENTS (priv->images); i++) { + GtkWidget *im = GTK_WIDGET (priv->images[i]); + gboolean enabled; + + /* add fudge factor so we can actually get 5 stars in reality */ + enabled = priv->rating >= rate_to_star[i] - 10; + + gtk_style_context_add_class (gtk_widget_get_style_context (im), + enabled ? "star-enabled" : "star-disabled"); + gtk_style_context_remove_class (gtk_widget_get_style_context (im), + enabled ? "star-disabled" : "star-enabled"); + } +} + +static void +gs_star_widget_refresh (GsStarWidget *star) +{ + GsStarWidgetPrivate *priv = gs_star_widget_get_instance_private (star); + + if (!gtk_widget_get_realized (GTK_WIDGET (star))) + return; + + /* remove all existing widgets */ + gs_container_remove_all (GTK_CONTAINER (priv->box1)); + + for (guint i = 0; i < G_N_ELEMENTS (priv->images); i++) { + GtkWidget *w; + GtkWidget *im; + + /* create image */ + im = gtk_image_new_from_icon_name ("starred-symbolic", + GTK_ICON_SIZE_DIALOG); + gtk_image_set_pixel_size (GTK_IMAGE (im), (gint) priv->icon_size); + priv->images[i] = GTK_IMAGE (im); + + /* create button */ + if (priv->interactive) { + w = gtk_button_new (); + g_signal_connect (w, "clicked", + G_CALLBACK (gs_star_widget_button_clicked_cb), star); + g_object_set_data (G_OBJECT (w), + "GsStarWidget::value", + GINT_TO_POINTER (rate_to_star[i])); + gtk_container_add (GTK_CONTAINER (w), im); + gtk_widget_set_visible (im, TRUE); + } else { + w = im; + } + gtk_widget_set_sensitive (w, priv->interactive); + gtk_style_context_add_class (gtk_widget_get_style_context (w), "star"); + gtk_widget_set_visible (w, TRUE); + gtk_container_add (GTK_CONTAINER (priv->box1), w); + } + + gs_star_widget_refresh_rating (star); +} + +void +gs_star_widget_set_interactive (GsStarWidget *star, gboolean interactive) +{ + GsStarWidgetPrivate *priv; + g_return_if_fail (GS_IS_STAR_WIDGET (star)); + priv = gs_star_widget_get_instance_private (star); + + if (priv->interactive == interactive) + return; + + priv->interactive = interactive; + g_object_notify_by_pspec (G_OBJECT (star), properties[PROP_INTERACTIVE]); + gs_star_widget_refresh (star); +} + +void +gs_star_widget_set_rating (GsStarWidget *star, + gint rating) +{ + GsStarWidgetPrivate *priv; + g_return_if_fail (GS_IS_STAR_WIDGET (star)); + priv = gs_star_widget_get_instance_private (star); + + if (priv->rating == rating) + return; + + priv->rating = rating; + g_object_notify_by_pspec (G_OBJECT (star), properties[PROP_RATING]); + gs_star_widget_refresh_rating (star); +} + +static void +gs_star_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsStarWidget *self = GS_STAR_WIDGET (object); + GsStarWidgetPrivate *priv = gs_star_widget_get_instance_private (self); + + switch ((GsStarWidgetProperty) prop_id) { + case PROP_ICON_SIZE: + g_value_set_uint (value, priv->icon_size); + break; + case PROP_INTERACTIVE: + g_value_set_boolean (value, priv->interactive); + break; + case PROP_RATING: + g_value_set_int (value, priv->rating); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_star_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsStarWidget *self = GS_STAR_WIDGET (object); + + switch ((GsStarWidgetProperty) prop_id) { + case PROP_ICON_SIZE: + gs_star_widget_set_icon_size (self, g_value_get_uint (value)); + break; + case PROP_INTERACTIVE: + gs_star_widget_set_interactive (self, g_value_get_boolean (value)); + break; + case PROP_RATING: + gs_star_widget_set_rating (self, g_value_get_int (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_star_widget_destroy (GtkWidget *widget) +{ + GTK_WIDGET_CLASS (gs_star_widget_parent_class)->destroy (widget); +} + +static void +gs_star_widget_realize (GtkWidget *widget) +{ + GTK_WIDGET_CLASS (gs_star_widget_parent_class)->realize (widget); + + /* Create child widgets. */ + gs_star_widget_refresh (GS_STAR_WIDGET (widget)); +} + +static void +gs_star_widget_init (GsStarWidget *star) +{ + gtk_widget_set_has_window (GTK_WIDGET (star), FALSE); + gtk_widget_init_template (GTK_WIDGET (star)); +} + +static void +gs_star_widget_class_init (GsStarWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = gs_star_widget_destroy; + widget_class->realize = gs_star_widget_realize; + object_class->get_property = gs_star_widget_get_property; + object_class->set_property = gs_star_widget_set_property; + + /** + * GsStarWidget:icon-size: + * + * Size of the star icons to use in the widget, in pixels. + * + * Since: 3.38 + */ + properties[PROP_ICON_SIZE] = + g_param_spec_uint ("icon-size", + "Icon Size", + "Size of icons to use, in pixels", + 0, G_MAXUINT, 12, + G_PARAM_READWRITE); + + /** + * GsStarWidget:interactive: + * + * Whether the widget accepts user input to change #GsStarWidget:rating. + * + * Since: 3.38 + */ + properties[PROP_INTERACTIVE] = + g_param_spec_boolean ("interactive", + "Interactive", + "Whether the rating is interactive", + FALSE, + G_PARAM_READWRITE); + + /** + * GsStarWidget:rating: + * + * The rating to display on the widget, as a percentage. `-1` indicates + * that the rating is unknown. + * + * Since: 3.38 + */ + properties[PROP_RATING] = + g_param_spec_int ("rating", + "Rating", + "Rating, out of 100%, or -1 for unknown", + -1, 100, -1, + G_PARAM_READWRITE); + + signals [RATING_CHANGED] = + g_signal_new ("rating-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsStarWidgetClass, rating_changed), + NULL, NULL, g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-star-widget.ui"); + gtk_widget_class_bind_template_child_private (widget_class, GsStarWidget, box1); +} + +GtkWidget * +gs_star_widget_new (void) +{ + GsStarWidget *star; + star = g_object_new (GS_TYPE_STAR_WIDGET, NULL); + return GTK_WIDGET (star); +} diff --git a/src/gs-star-widget.h b/src/gs-star-widget.h new file mode 100644 index 0000000..baa1382 --- /dev/null +++ b/src/gs-star-widget.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_STAR_WIDGET (gs_star_widget_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsStarWidget, gs_star_widget, GS, STAR_WIDGET, GtkBin) + +struct _GsStarWidgetClass +{ + GtkBinClass parent_class; + + void (*rating_changed) (GsStarWidget *star); +}; + +GtkWidget *gs_star_widget_new (void); +gint gs_star_widget_get_rating (GsStarWidget *star); +void gs_star_widget_set_rating (GsStarWidget *star, + gint rating); +void gs_star_widget_set_icon_size (GsStarWidget *star, + guint pixel_size); +void gs_star_widget_set_interactive (GsStarWidget *star, + gboolean interactive); + +G_END_DECLS diff --git a/src/gs-star-widget.ui b/src/gs-star-widget.ui new file mode 100644 index 0000000..21621d1 --- /dev/null +++ b/src/gs-star-widget.ui @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsStarWidget" parent="GtkBin"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="spacing">2</property> + <style> + <class name="star"/> + </style> + <child> + <placeholder/> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-summary-tile.c b/src/gs-summary-tile.c new file mode 100644 index 0000000..c2ed8f9 --- /dev/null +++ b/src/gs-summary-tile.c @@ -0,0 +1,247 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-summary-tile.h" +#include "gs-common.h" + +struct _GsSummaryTile +{ + GsAppTile parent_instance; + GtkWidget *image; + GtkWidget *name; + GtkWidget *summary; + GtkWidget *eventbox; + GtkWidget *stack; + gint preferred_width; + GtkCssProvider *tile_provider; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsSummaryTile, gs_summary_tile, GS_TYPE_APP_TILE) + +enum { + PROP_0, + PROP_PREFERRED_WIDTH +}; + +static void +gs_summary_tile_refresh (GsAppTile *self) +{ + GsSummaryTile *tile = GS_SUMMARY_TILE (self); + GsApp *app = gs_app_tile_get_app (self); + AtkObject *accessible; + GtkStyleContext *context; + const GdkPixbuf *pixbuf; + gboolean installed; + g_autofree gchar *name = NULL; + const gchar *summary; + const gchar *css; + + if (app == NULL) + return; + + gtk_image_set_pixel_size (GTK_IMAGE (tile->image), 64); + gtk_stack_set_visible_child_name (GTK_STACK (tile->stack), "content"); + + /* set name */ + gtk_label_set_label (GTK_LABEL (tile->name), gs_app_get_name (app)); + + summary = gs_app_get_summary (app); + gtk_label_set_label (GTK_LABEL (tile->summary), summary); + gtk_widget_set_visible (tile->summary, summary && summary[0]); + + pixbuf = gs_app_get_pixbuf (app); + if (pixbuf != NULL) { + gs_image_set_from_pixbuf (GTK_IMAGE (tile->image), pixbuf); + } else { + gtk_image_set_from_icon_name (GTK_IMAGE (tile->image), + "application-x-executable", + GTK_ICON_SIZE_DIALOG); + } + context = gtk_widget_get_style_context (tile->image); + if (gs_app_get_use_drop_shadow (app)) + gtk_style_context_add_class (context, "icon-dropshadow"); + else + gtk_style_context_remove_class (context, "icon-dropshadow"); + + /* perhaps set custom css */ + css = gs_app_get_metadata_item (app, "GnomeSoftware::AppTile-css"); + gs_utils_widget_set_css (GTK_WIDGET (tile), &tile->tile_provider, "summary-tile", css); + + accessible = gtk_widget_get_accessible (GTK_WIDGET (tile)); + + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + installed = TRUE; + name = g_strdup_printf (_("%s (Installed)"), + gs_app_get_name (app)); + break; + case AS_APP_STATE_INSTALLING: + installed = FALSE; + name = g_strdup_printf (_("%s (Installing)"), + gs_app_get_name (app)); + break; + case AS_APP_STATE_REMOVING: + installed = TRUE; + name = g_strdup_printf (_("%s (Removing)"), + gs_app_get_name (app)); + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + case AS_APP_STATE_AVAILABLE: + default: + installed = FALSE; + name = g_strdup (gs_app_get_name (app)); + break; + } + + gtk_widget_set_visible (tile->eventbox, installed); + + if (GTK_IS_ACCESSIBLE (accessible) && name != NULL) { + atk_object_set_name (accessible, name); + atk_object_set_description (accessible, gs_app_get_summary (app)); + } +} + +static void +gs_summary_tile_init (GsSummaryTile *tile) +{ + gtk_widget_set_has_window (GTK_WIDGET (tile), FALSE); + tile->preferred_width = -1; +// gtk_image_clear (GTK_IMAGE (tile->image)); + gtk_widget_init_template (GTK_WIDGET (tile)); +} + +static void +gs_summary_tile_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsSummaryTile *app_tile = GS_SUMMARY_TILE (object); + + switch (prop_id) { + case PROP_PREFERRED_WIDTH: + g_value_set_int (value, app_tile->preferred_width); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_summary_tile_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsSummaryTile *app_tile = GS_SUMMARY_TILE (object); + + switch (prop_id) { + case PROP_PREFERRED_WIDTH: + app_tile->preferred_width = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (app_tile)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_summary_tile_dispose (GObject *object) +{ + GsSummaryTile *tile = GS_SUMMARY_TILE (object); + + g_clear_object (&tile->tile_provider); + + G_OBJECT_CLASS (gs_summary_tile_parent_class)->dispose (object); +} + +static void +gs_app_get_preferred_width (GtkWidget *widget, + gint *min, gint *nat) +{ + gint m; + GsSummaryTile *app_tile = GS_SUMMARY_TILE (widget); + + if (app_tile->preferred_width < 0) { + /* Just retrieve the default values */ + GTK_WIDGET_CLASS (gs_summary_tile_parent_class)->get_preferred_width (widget, min, nat); + return; + } + + GTK_WIDGET_CLASS (gs_summary_tile_parent_class)->get_preferred_width (widget, &m, NULL); + + if (min != NULL) + *min = m; + if (nat != NULL) + *nat = MAX (m, app_tile->preferred_width); +} + +static void +gs_summary_tile_class_init (GsSummaryTileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GsAppTileClass *tile_class = GS_APP_TILE_CLASS (klass); + + object_class->get_property = gs_summary_tile_get_property; + object_class->set_property = gs_summary_tile_set_property; + object_class->dispose = gs_summary_tile_dispose; + + widget_class->get_preferred_width = gs_app_get_preferred_width; + + tile_class->refresh = gs_summary_tile_refresh; + + /** + * GsAppTile:preferred-width: + * + * The only purpose of this property is to be retrieved as the + * natural width by gtk_widget_get_preferred_width() fooling the + * parent #GtkFlowBox container and making it switch to more columns + * (children per row) if it is able to place n+1 children in a row + * having this specified width. If this value is less than a minimum + * width of this app tile then the minimum is returned instead. Set + * this property to -1 to turn off this feature and return the default + * natural width instead. + */ + g_object_class_install_property (object_class, PROP_PREFERRED_WIDTH, + g_param_spec_int ("preferred-width", + "Preferred width", + "The preferred width of this widget, its only purpose is to trick the parent container", + -1, G_MAXINT, -1, + G_PARAM_READWRITE)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-summary-tile.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsSummaryTile, + image); + gtk_widget_class_bind_template_child (widget_class, GsSummaryTile, + name); + gtk_widget_class_bind_template_child (widget_class, GsSummaryTile, + summary); + gtk_widget_class_bind_template_child (widget_class, GsSummaryTile, + eventbox); + gtk_widget_class_bind_template_child (widget_class, GsSummaryTile, + stack); +} + +GtkWidget * +gs_summary_tile_new (GsApp *cat) +{ + GsAppTile *tile = g_object_new (GS_TYPE_SUMMARY_TILE, NULL); + gs_app_tile_set_app (tile, cat); + return GTK_WIDGET (tile); +} diff --git a/src/gs-summary-tile.h b/src/gs-summary-tile.h new file mode 100644 index 0000000..c9c16ef --- /dev/null +++ b/src/gs-summary-tile.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app-tile.h" + +G_BEGIN_DECLS + +#define GS_TYPE_SUMMARY_TILE (gs_summary_tile_get_type ()) + +G_DECLARE_FINAL_TYPE (GsSummaryTile, gs_summary_tile, GS, SUMMARY_TILE, GsAppTile) + +GtkWidget *gs_summary_tile_new (GsApp *app); + +G_END_DECLS diff --git a/src/gs-summary-tile.ui b/src/gs-summary-tile.ui new file mode 100644 index 0000000..a63bbd6 --- /dev/null +++ b/src/gs-summary-tile.ui @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsSummaryTile" parent="GsAppTile"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <!-- This is the minimum (sic!) width of a tile when the GtkFlowBox parent container switches to 3 columns --> + <property name="preferred-width">270</property> + <style> + <class name="view"/> + <class name="tile"/> + </style> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="waiting"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="icon-name">content-loading-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="name">waiting</property> + </packing> + </child> + <child> + <object class="GtkOverlay" id="overlay"> + <property name="visible">True</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <child type="overlay"> + <object class="GtkEventBox" id="eventbox"> + <property name="visible">False</property> + <property name="no_show_all">True</property> + <property name="visible_window">True</property> + <property name="halign">end</property> + <property name="valign">start</property> + <child> + <object class="GtkImage" id="installed-icon"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="pixel-size">16</property> + <property name="margin-top">6</property> + <property name="margin-right">6</property> + <property name="icon-name">software-installed-symbolic</property> + <style> + <class name="installed-icon"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="margin-top">17</property> + <property name="margin-bottom">17</property> + <property name="margin-start">17</property> + <property name="margin-end">17</property> + <property name="row-spacing">1</property> + <property name="column-spacing">14</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="width-request">64</property> + <property name="height-request">64</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="name"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + <style> + <class name="app-tile-label"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary"> + <property name="visible">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <property name="yalign">0.0</property> + <property name="lines">2</property> + <property name="vexpand">True</property> + <property name="single-line-mode">True</property> + <style> + <class name="app-tile-label"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">content</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-third-party-repo-row.c b/src/gs-third-party-repo-row.c new file mode 100644 index 0000000..e1da3fc --- /dev/null +++ b/src/gs-third-party-repo-row.c @@ -0,0 +1,247 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-third-party-repo-row.h" + +#include "gs-progress-button.h" +#include <glib/gi18n.h> + +typedef struct +{ + GsApp *app; + + GtkWidget *button; + GtkWidget *comment_label; + GtkWidget *name_label; + guint refresh_idle_id; +} GsThirdPartyRepoRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsThirdPartyRepoRow, gs_third_party_repo_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + SIGNAL_BUTTON_CLICKED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +void +gs_third_party_repo_row_set_name (GsThirdPartyRepoRow *row, const gchar *name) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + gtk_label_set_text (GTK_LABEL (priv->name_label), name); +} + +void +gs_third_party_repo_row_set_comment (GsThirdPartyRepoRow *row, const gchar *comment) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + gtk_label_set_markup (GTK_LABEL (priv->comment_label), comment); +} + +static void +refresh_ui (GsThirdPartyRepoRow *row) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + GtkStyleContext *context; + + if (priv->app == NULL) + return; + + /* do a fill bar for the current progress */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_INSTALLING: + gs_progress_button_set_progress (GS_PROGRESS_BUTTON (priv->button), + gs_app_get_progress (priv->app)); + gs_progress_button_set_show_progress (GS_PROGRESS_BUTTON (priv->button), TRUE); + break; + default: + gs_progress_button_set_show_progress (GS_PROGRESS_BUTTON (priv->button), FALSE); + break; + } + + /* set button text */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_UNAVAILABLE: + /* TRANSLATORS: this is a button in the software repositories + dialog for installing a repo. + The ellipsis indicates that further steps are required */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Install…")); + /* enable button */ + gtk_widget_set_sensitive (priv->button, TRUE); + break; + case AS_APP_STATE_AVAILABLE: + case AS_APP_STATE_AVAILABLE_LOCAL: + /* TRANSLATORS: this is a button in the software repositories + dialog for installing a repo */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Install")); + /* enable button */ + gtk_widget_set_sensitive (priv->button, TRUE); + break; + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + /* TRANSLATORS: this is a button in the software repositories + dialog for removing multiple repos */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("_Remove All")); + /* enable button */ + gtk_widget_set_sensitive (priv->button, TRUE); + break; + case AS_APP_STATE_INSTALLING: + /* TRANSLATORS: this is a button in the software repositories dialog + that shows the status of a repo being installed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Installing")); + /* disable button */ + gtk_widget_set_sensitive (priv->button, FALSE); + break; + case AS_APP_STATE_REMOVING: + /* TRANSLATORS: this is a button in the software repositories dialog + that shows the status of a repo being removed */ + gtk_button_set_label (GTK_BUTTON (priv->button), _("Removing")); + /* disable button */ + gtk_widget_set_sensitive (priv->button, FALSE); + break; + default: + break; + } + + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_QUEUED_FOR_INSTALL: + gtk_widget_set_visible (priv->button, FALSE); + break; + default: + gtk_widget_set_visible (priv->button, TRUE); + break; + } + + context = gtk_widget_get_style_context (priv->button); + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_INSTALLED: + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + gtk_style_context_add_class (context, "destructive-action"); + break; + default: + gtk_style_context_remove_class (context, "destructive-action"); + break; + } +} + +static gboolean +refresh_idle (gpointer user_data) +{ + g_autoptr(GsThirdPartyRepoRow) row = (GsThirdPartyRepoRow *) user_data; + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + + refresh_ui (row); + + priv->refresh_idle_id = 0; + return G_SOURCE_REMOVE; +} + +static void +app_state_changed_cb (GsApp *repo, GParamSpec *pspec, GsThirdPartyRepoRow *row) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + + if (priv->refresh_idle_id > 0) + return; + priv->refresh_idle_id = g_idle_add (refresh_idle, g_object_ref (row)); +} + +void +gs_third_party_repo_row_set_app (GsThirdPartyRepoRow *row, GsApp *app) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + + if (priv->app != NULL) + g_signal_handlers_disconnect_by_func (priv->app, app_state_changed_cb, row); + + g_set_object (&priv->app, app); + if (priv->app != NULL) { + g_signal_connect_object (priv->app, "notify::state", + G_CALLBACK (app_state_changed_cb), + row, 0); + g_signal_connect_object (priv->app, "notify::progress", + G_CALLBACK (app_state_changed_cb), + row, 0); + refresh_ui (row); + } +} + +GsApp * +gs_third_party_repo_row_get_app (GsThirdPartyRepoRow *row) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + return priv->app; +} + +static void +button_clicked_cb (GtkWidget *widget, GsThirdPartyRepoRow *row) +{ + g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0); +} + +static void +gs_third_party_repo_row_destroy (GtkWidget *object) +{ + GsThirdPartyRepoRow *row = GS_THIRD_PARTY_REPO_ROW (object); + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + + if (priv->app != NULL) { + g_signal_handlers_disconnect_by_func (priv->app, app_state_changed_cb, row); + g_clear_object (&priv->app); + } + + if (priv->refresh_idle_id != 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + GTK_WIDGET_CLASS (gs_third_party_repo_row_parent_class)->destroy (object); +} + +static void +gs_third_party_repo_row_init (GsThirdPartyRepoRow *row) +{ + GsThirdPartyRepoRowPrivate *priv = gs_third_party_repo_row_get_instance_private (row); + + gtk_widget_init_template (GTK_WIDGET (row)); + g_signal_connect (priv->button, "clicked", + G_CALLBACK (button_clicked_cb), row); +} + +static void +gs_third_party_repo_row_class_init (GsThirdPartyRepoRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + widget_class->destroy = gs_third_party_repo_row_destroy; + + signals [SIGNAL_BUTTON_CLICKED] = + g_signal_new ("button-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsThirdPartyRepoRowClass, button_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-third-party-repo-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsThirdPartyRepoRow, button); + gtk_widget_class_bind_template_child_private (widget_class, GsThirdPartyRepoRow, comment_label); + gtk_widget_class_bind_template_child_private (widget_class, GsThirdPartyRepoRow, name_label); +} + +GtkWidget * +gs_third_party_repo_row_new (void) +{ + return g_object_new (GS_TYPE_THIRD_PARTY_REPO_ROW, NULL); +} diff --git a/src/gs-third-party-repo-row.h b/src/gs-third-party-repo-row.h new file mode 100644 index 0000000..91d1552 --- /dev/null +++ b/src/gs-third-party-repo-row.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gnome-software-private.h" +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_THIRD_PARTY_REPO_ROW (gs_third_party_repo_row_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsThirdPartyRepoRow, gs_third_party_repo_row, GS, THIRD_PARTY_REPO_ROW, GtkListBoxRow) + +struct _GsThirdPartyRepoRowClass +{ + GtkListBoxRowClass parent_class; + void (*button_clicked) (GsThirdPartyRepoRow *row); +}; + +GtkWidget *gs_third_party_repo_row_new (void); +void gs_third_party_repo_row_set_name (GsThirdPartyRepoRow *row, + const gchar *name); +void gs_third_party_repo_row_set_comment (GsThirdPartyRepoRow *row, + const gchar *comment); +void gs_third_party_repo_row_set_app (GsThirdPartyRepoRow *row, + GsApp *app); +GsApp *gs_third_party_repo_row_get_app (GsThirdPartyRepoRow *row); + +G_END_DECLS diff --git a/src/gs-third-party-repo-row.ui b/src/gs-third-party-repo-row.ui new file mode 100644 index 0000000..68ba9b1 --- /dev/null +++ b/src/gs-third-party-repo-row.ui @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsThirdPartyRepoRow" parent="GtkListBoxRow"> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="orientation">horizontal</property> + <property name="spacing">16</property> + <child> + <object class="GtkBox" id="vbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="hexpand">True</property> + <child> + <object class="GtkBox" id="hbox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="spacing">4</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="comment_label"> + <property name="visible">True</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GsProgressButton" id="button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="width_request">105</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-update-dialog.c b/src/gs-update-dialog.c new file mode 100644 index 0000000..cd3c0b6 --- /dev/null +++ b/src/gs-update-dialog.c @@ -0,0 +1,842 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-update-dialog.h" +#include "gs-app-row.h" +#include "gs-update-list.h" +#include "gs-common.h" + +typedef struct { + gchar *title; + gchar *stack_page; + GtkWidget *focus; +} BackEntry; + +typedef enum { + GS_UPDATE_DIALOG_SECTION_ADDITIONS, + GS_UPDATE_DIALOG_SECTION_REMOVALS, + GS_UPDATE_DIALOG_SECTION_UPDATES, + GS_UPDATE_DIALOG_SECTION_DOWNGRADES, + GS_UPDATE_DIALOG_SECTION_LAST, +} GsUpdateDialogSection; + +struct _GsUpdateDialog +{ + GtkDialog parent_instance; + + GQueue *back_entry_stack; + GCancellable *cancellable; + GsPluginLoader *plugin_loader; + GtkWidget *box_header; + GtkWidget *button_back; + GtkWidget *image_icon; + GtkWidget *label_details; + GtkWidget *label_name; + GtkWidget *label_summary; + GtkWidget *list_boxes[GS_UPDATE_DIALOG_SECTION_LAST]; + GtkWidget *list_box_installed_updates; + GtkWidget *os_update_description; + GtkWidget *os_update_box; + GtkWidget *scrolledwindow; + GtkWidget *scrolledwindow_details; + GtkWidget *spinner; + GtkWidget *stack; + GtkWidget *permissions_section_box; + GtkWidget *permissions_section_content; +}; + +G_DEFINE_TYPE (GsUpdateDialog, gs_update_dialog, GTK_TYPE_DIALOG) + +static void +save_back_entry (GsUpdateDialog *dialog) +{ + BackEntry *entry; + + entry = g_slice_new0 (BackEntry); + entry->stack_page = g_strdup (gtk_stack_get_visible_child_name (GTK_STACK (dialog->stack))); + entry->title = g_strdup (gtk_window_get_title (GTK_WINDOW (dialog))); + + entry->focus = gtk_window_get_focus (GTK_WINDOW (dialog)); + if (entry->focus != NULL) + g_object_add_weak_pointer (G_OBJECT (entry->focus), + (gpointer *) &entry->focus); + + g_queue_push_head (dialog->back_entry_stack, entry); +} + +static void +back_entry_free (BackEntry *entry) +{ + if (entry->focus != NULL) + g_object_remove_weak_pointer (G_OBJECT (entry->focus), + (gpointer *) &entry->focus); + g_free (entry->stack_page); + g_free (entry->title); + g_slice_free (BackEntry, entry); +} + +static struct { + GsAppPermissions permission; + const char *title; + const char *subtitle; +} permission_display_data[] = { + { GS_APP_PERMISSIONS_NETWORK, N_("Network"), N_("Can communicate over the network") }, + { GS_APP_PERMISSIONS_SYSTEM_BUS, N_("System Services"), N_("Can access D-Bus services on the system bus") }, + { GS_APP_PERMISSIONS_SESSION_BUS, N_("Session Services"), N_("Can access D-Bus services on the session bus") }, + { GS_APP_PERMISSIONS_DEVICES, N_("Devices"), N_("Can access system device files") }, + { GS_APP_PERMISSIONS_HOME_FULL, N_("Home folder"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_HOME_READ, N_("Home folder"), N_("Can view files") }, + { GS_APP_PERMISSIONS_FILESYSTEM_FULL, N_("File system"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_FILESYSTEM_READ, N_("File system"), N_("Can view files") }, + { GS_APP_PERMISSIONS_DOWNLOADS_FULL, N_("Downloads folder"), N_("Can view, edit and create files") }, + { GS_APP_PERMISSIONS_DOWNLOADS_READ, N_("Downloads folder"), N_("Can view files") }, + { GS_APP_PERMISSIONS_SETTINGS, N_("Settings"), N_("Can view and change any settings") }, + { GS_APP_PERMISSIONS_X11, N_("Legacy display system"), N_("Uses an old, insecure display system") }, + { GS_APP_PERMISSIONS_ESCAPE_SANDBOX, N_("Sandbox escape"), N_("Can escape the sandbox and circumvent any other restrictions") }, +}; + +static void +populate_permissions_section (GsUpdateDialog *dialog, GsAppPermissions permissions) +{ + GList *children; + + children = gtk_container_get_children (GTK_CONTAINER (dialog->permissions_section_content)); + for (GList *l = children; l != NULL; l = l->next) + gtk_widget_destroy (GTK_WIDGET (l->data)); + g_list_free (children); + + for (gsize i = 0; i < G_N_ELEMENTS (permission_display_data); i++) { + GtkWidget *row, *image, *box, *label; + + if ((permissions & permission_display_data[i].permission) == 0) + continue; + + row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_widget_show (row); + if ((permission_display_data[i].permission & ~MEDIUM_PERMISSIONS) != 0) { + gtk_style_context_add_class (gtk_widget_get_style_context (row), "permission-row-warning"); + } + + image = gtk_image_new_from_icon_name ("dialog-warning-symbolic", GTK_ICON_SIZE_MENU); + if ((permission_display_data[i].permission & ~MEDIUM_PERMISSIONS) == 0) + gtk_widget_set_opacity (image, 0); + + gtk_widget_show (image); + gtk_container_add (GTK_CONTAINER (row), image); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show (box); + gtk_container_add (GTK_CONTAINER (row), box); + + label = gtk_label_new (_(permission_display_data[i].title)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (box), label); + + label = gtk_label_new (_(permission_display_data[i].subtitle)); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_widget_show (label); + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_container_add (GTK_CONTAINER (dialog->permissions_section_content), row); + } +} + +static void +set_updates_description_ui (GsUpdateDialog *dialog, GsApp *app) +{ + AsAppKind kind; + const GdkPixbuf *pixbuf; + const gchar *update_details; + + /* set window title */ + kind = gs_app_get_kind (app); + if (kind == AS_APP_KIND_OS_UPDATE) { + gtk_window_set_title (GTK_WINDOW (dialog), gs_app_get_name (app)); + } else if (gs_app_get_source_default (app) != NULL && + gs_app_get_update_version (app) != NULL) { + g_autofree gchar *tmp = NULL; + tmp = g_strdup_printf ("%s %s", + gs_app_get_source_default (app), + gs_app_get_update_version (app)); + gtk_window_set_title (GTK_WINDOW (dialog), tmp); + } else if (gs_app_get_source_default (app) != NULL) { + gtk_window_set_title (GTK_WINDOW (dialog), + gs_app_get_source_default (app)); + } else { + gtk_window_set_title (GTK_WINDOW (dialog), + gs_app_get_update_version (app)); + } + + /* set update header */ + gtk_widget_set_visible (dialog->box_header, kind == AS_APP_KIND_DESKTOP); + update_details = gs_app_get_update_details (app); + if (update_details == NULL) { + /* TRANSLATORS: this is where the packager did not write + * a description for the update */ + update_details = _("No update description available."); + } + gtk_label_set_label (GTK_LABEL (dialog->label_details), update_details); + gtk_label_set_label (GTK_LABEL (dialog->label_name), gs_app_get_name (app)); + gtk_label_set_label (GTK_LABEL (dialog->label_summary), gs_app_get_summary (app)); + + pixbuf = gs_app_get_pixbuf (app); + if (pixbuf != NULL) + gs_image_set_from_pixbuf (GTK_IMAGE (dialog->image_icon), pixbuf); + + /* show the back button if needed */ + gtk_widget_set_visible (dialog->button_back, !g_queue_is_empty (dialog->back_entry_stack)); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS)) { + gtk_widget_show (dialog->permissions_section_box); + populate_permissions_section (dialog, gs_app_get_update_permissions (app)); + } else { + gtk_widget_hide (dialog->permissions_section_box); + } +} + +static void +row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsUpdateDialog *dialog) +{ + GsApp *app; + + app = GS_APP (g_object_get_data (G_OBJECT (gtk_bin_get_child (GTK_BIN (row))), "app")); + + /* save the current stack state for the back button */ + save_back_entry (dialog); + + /* setup package view */ + gs_update_dialog_show_update_details (dialog, app); +} + +static void +installed_updates_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsUpdateDialog *dialog) +{ + GsApp *app; + + app = gs_app_row_get_app (GS_APP_ROW (row)); + + /* save the current stack state for the back button */ + save_back_entry (dialog); + + gs_update_dialog_show_update_details (dialog, app); +} + +static void +get_installed_updates_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsUpdateDialog *dialog) +{ + guint i; + guint64 install_date; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) error = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + + /* if we're in teardown, short-circuit and return immediately without + * dereferencing priv variables */ + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + dialog->spinner == NULL) { + g_debug ("get installed updates cancelled"); + return; + } + + gs_stop_spinner (GTK_SPINNER (dialog->spinner)); + + /* error */ + if (list == NULL) { + g_warning ("failed to get installed updates: %s", error->message); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "empty"); + return; + } + + /* no results */ + if (gs_app_list_length (list) == 0) { + g_debug ("no installed updates to show"); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "empty"); + return; + } + + /* set the header title using any one of the applications */ + install_date = gs_app_get_install_date (gs_app_list_index (list, 0)); + if (install_date > 0) { + GtkWidget *header; + g_autoptr(GDateTime) date = NULL; + g_autofree gchar *date_str = NULL; + g_autofree gchar *subtitle = NULL; + + date = g_date_time_new_from_unix_utc ((gint64) install_date); + date_str = g_date_time_format (date, "%x"); + + /* TRANSLATORS: this is the subtitle of the installed updates dialog window. + %s will be replaced by the date when the updates were installed. + The date format is defined by the locale's preferred date representation + ("%x" in strftime.) */ + subtitle = g_strdup_printf (_("Installed on %s"), date_str); + header = gtk_dialog_get_header_bar (GTK_DIALOG (dialog)); + gtk_header_bar_set_subtitle (GTK_HEADER_BAR (header), subtitle); + } + + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "installed-updates-list"); + + gs_container_remove_all (GTK_CONTAINER (dialog->list_box_installed_updates)); + for (i = 0; i < gs_app_list_length (list); i++) { + gs_update_list_add_app (GS_UPDATE_LIST (dialog->list_box_installed_updates), + gs_app_list_index (list, i)); + } +} + +void +gs_update_dialog_show_installed_updates (GsUpdateDialog *dialog) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* TRANSLATORS: this is the title of the installed updates dialog window */ + gtk_window_set_title (GTK_WINDOW (dialog), _("Installed Updates")); + + gtk_widget_set_visible (dialog->button_back, !g_queue_is_empty (dialog->back_entry_stack)); + gs_start_spinner (GTK_SPINNER (dialog->spinner)); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "spinner"); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, + NULL); + gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job, + dialog->cancellable, + (GAsyncReadyCallback) get_installed_updates_cb, + dialog); +} + +static void +unset_focus (GtkWidget *widget) +{ + GtkWidget *focus; + + focus = gtk_window_get_focus (GTK_WINDOW (widget)); + if (GTK_IS_LABEL (focus)) + gtk_label_select_region (GTK_LABEL (focus), 0, 0); + gtk_window_set_focus (GTK_WINDOW (widget), NULL); +} + +static gchar * +format_version_update (GsApp *app) +{ + const gchar *tmp; + const gchar *version_current = NULL; + const gchar *version_update = NULL; + + /* current version */ + tmp = gs_app_get_version (app); + if (tmp != NULL && tmp[0] != '\0') + version_current = tmp; + + /* update version */ + tmp = gs_app_get_update_version (app); + if (tmp != NULL && tmp[0] != '\0') + version_update = tmp; + + /* have both */ + if (version_current != NULL && version_update != NULL && + g_strcmp0 (version_current, version_update) != 0) { + return g_strdup_printf ("%s → %s", + version_current, + version_update); + } + + /* just update */ + if (version_update) + return g_strdup (version_update); + + /* we have nothing, nada, zilch */ + return NULL; +} + +static GtkWidget * +create_app_row (GsApp *app) +{ + GtkWidget *row, *label; + + row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + g_object_set_data_full (G_OBJECT (row), + "app", + g_object_ref (app), + g_object_unref); + label = gtk_label_new (gs_app_get_source_default (app)); + g_object_set (label, + "margin-start", 20, + "margin-end", 0, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 0.0, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_hexpand (label, TRUE); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_container_add (GTK_CONTAINER (row), label); + if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE || + gs_app_get_state (app) == AS_APP_STATE_UPDATABLE_LIVE) { + g_autofree gchar *verstr = format_version_update (app); + label = gtk_label_new (verstr); + } else { + label = gtk_label_new (gs_app_get_version (app)); + } + g_object_set (label, + "margin-start", 0, + "margin-end", 20, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 1.0, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_widget_set_halign (label, GTK_ALIGN_END); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_container_add (GTK_CONTAINER (row), label); + gtk_widget_show_all (row); + + return row; +} + +static gboolean +is_downgrade (const gchar *evr1, + const gchar *evr2) +{ + gint rc; + g_autofree gchar *epoch1 = NULL; + g_autofree gchar *epoch2 = NULL; + g_autofree gchar *version1 = NULL; + g_autofree gchar *version2 = NULL; + g_autofree gchar *release1 = NULL; + g_autofree gchar *release2 = NULL; + + if (evr1 == NULL || evr2 == NULL) + return FALSE; + + /* split into epoch-version-release */ + if (!gs_utils_parse_evr (evr1, &epoch1, &version1, &release1)) + return FALSE; + if (!gs_utils_parse_evr (evr2, &epoch2, &version2, &release2)) + return FALSE; + + /* ignore epoch here as it's a way to make downgrades happen and not + * part of the semantic version */ + + /* check version */ +#if AS_CHECK_VERSION(0,7,15) + rc = as_utils_vercmp_full (version1, version2, + AS_VERSION_COMPARE_FLAG_NONE); +#else + rc = as_utils_vercmp (version1, version2); +#endif + if (rc != 0) + return rc > 0; + + /* check release */ +#if AS_CHECK_VERSION(0,7,15) + rc = as_utils_vercmp_full (version1, version2, + AS_VERSION_COMPARE_FLAG_NONE); +#else + rc = as_utils_vercmp (release1, release2); +#endif + if (rc != 0) + return rc > 0; + + return FALSE; +} + +static GsUpdateDialogSection +get_app_section (GsApp *app) +{ + GsUpdateDialogSection section; + + /* Sections: + * 1. additions + * 2. removals + * 3. updates + * 4. downgrades */ + switch (gs_app_get_state (app)) { + case AS_APP_STATE_AVAILABLE: + section = GS_UPDATE_DIALOG_SECTION_ADDITIONS; + break; + case AS_APP_STATE_UNAVAILABLE: + section = GS_UPDATE_DIALOG_SECTION_REMOVALS; + break; + case AS_APP_STATE_UPDATABLE: + case AS_APP_STATE_UPDATABLE_LIVE: + if (is_downgrade (gs_app_get_version (app), + gs_app_get_update_version (app))) + section = GS_UPDATE_DIALOG_SECTION_DOWNGRADES; + else + section = GS_UPDATE_DIALOG_SECTION_UPDATES; + break; + default: + g_warning ("get_app_section: unhandled state %s for %s", + as_app_state_to_string (gs_app_get_state (app)), + gs_app_get_unique_id (app)); + section = GS_UPDATE_DIALOG_SECTION_UPDATES; + break; + } + + return section; +} + +static gint +os_updates_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GObject *o1 = G_OBJECT (gtk_bin_get_child (GTK_BIN (a))); + GObject *o2 = G_OBJECT (gtk_bin_get_child (GTK_BIN (b))); + GsApp *a1 = g_object_get_data (o1, "app"); + GsApp *a2 = g_object_get_data (o2, "app"); + const gchar *key1 = gs_app_get_source_default (a1); + const gchar *key2 = gs_app_get_source_default (a2); + + return g_strcmp0 (key1, key2); +} + +static GtkWidget * +get_section_header (GsUpdateDialog *dialog, GsUpdateDialogSection section) +{ + GtkStyleContext *context; + GtkWidget *header; + GtkWidget *label; + + /* get labels and buttons for everything */ + if (section == GS_UPDATE_DIALOG_SECTION_ADDITIONS) { + /* TRANSLATORS: This is the header for package additions during + * a system update */ + label = gtk_label_new (_("Additions")); + } else if (section == GS_UPDATE_DIALOG_SECTION_REMOVALS) { + /* TRANSLATORS: This is the header for package removals during + * a system update */ + label = gtk_label_new (_("Removals")); + } else if (section == GS_UPDATE_DIALOG_SECTION_UPDATES) { + /* TRANSLATORS: This is the header for package updates during + * a system update */ + label = gtk_label_new (_("Updates")); + } else if (section == GS_UPDATE_DIALOG_SECTION_DOWNGRADES) { + /* TRANSLATORS: This is the header for package downgrades during + * a system update */ + label = gtk_label_new (_("Downgrades")); + } else { + g_assert_not_reached (); + } + + /* create header */ + header = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + context = gtk_widget_get_style_context (header); + gtk_style_context_add_class (context, "app-listbox-header"); + + /* put label into the header */ + gtk_widget_set_hexpand (label, TRUE); + gtk_container_add (GTK_CONTAINER (header), label); + gtk_widget_set_visible (label, TRUE); + gtk_widget_set_margin_start (label, 6); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + context = gtk_widget_get_style_context (label); + gtk_style_context_add_class (context, "app-listbox-header-title"); + + /* success */ + return header; +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GsUpdateDialog *dialog = (GsUpdateDialog *) user_data; + GObject *o = G_OBJECT (gtk_bin_get_child (GTK_BIN (row))); + GsApp *app = g_object_get_data (o, "app"); + GtkWidget *header = NULL; + + if (before == NULL) + header = get_section_header (dialog, get_app_section (app)); + else + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static void +create_section (GsUpdateDialog *dialog, GsUpdateDialogSection section) +{ + GtkStyleContext *context; + + dialog->list_boxes[section] = gtk_list_box_new (); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (dialog->list_boxes[section]), + GTK_SELECTION_NONE); + gtk_list_box_set_sort_func (GTK_LIST_BOX (dialog->list_boxes[section]), + os_updates_sort_func, + dialog, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (dialog->list_boxes[section]), + list_header_func, + dialog, NULL); + g_signal_connect (GTK_LIST_BOX (dialog->list_boxes[section]), "row-activated", + G_CALLBACK (row_activated_cb), dialog); + gtk_widget_set_visible (dialog->list_boxes[section], TRUE); + gtk_widget_set_vexpand (dialog->list_boxes[section], TRUE); + gtk_container_add (GTK_CONTAINER (dialog->os_update_box), dialog->list_boxes[section]); + gtk_widget_set_margin_top (dialog->list_boxes[section], 24); + + /* reorder the children */ + for (guint i = 0; i < GS_UPDATE_DIALOG_SECTION_LAST; i++) { + if (dialog->list_boxes[i] == NULL) + continue; + gtk_box_reorder_child (GTK_BOX (dialog->os_update_box), + dialog->list_boxes[i], i); + } + + /* make rounded edges */ + context = gtk_widget_get_style_context (dialog->list_boxes[section]); + gtk_style_context_add_class (context, "app-updates-section"); +} + +void +gs_update_dialog_show_update_details (GsUpdateDialog *dialog, GsApp *app) +{ + AsAppKind kind; + g_autofree gchar *str = NULL; + + /* debug */ + str = gs_app_to_string (app); + g_debug ("%s", str); + + /* set update header */ + set_updates_description_ui (dialog, app); + + /* workaround a gtk+ issue where the dialog comes up with a label selected, + * https://bugzilla.gnome.org/show_bug.cgi?id=734033 */ + unset_focus (GTK_WIDGET (dialog)); + + /* set update description */ + kind = gs_app_get_kind (app); + if (kind == AS_APP_KIND_OS_UPDATE) { + GsAppList *related; + GsApp *app_related; + GsUpdateDialogSection section; + GtkWidget *row; + + gtk_label_set_text (GTK_LABEL (dialog->os_update_description), + gs_app_get_description (app)); + + /* clear existing data */ + for (guint i = 0; i < GS_UPDATE_DIALOG_SECTION_LAST; i++) { + if (dialog->list_boxes[i] == NULL) + continue; + gs_container_remove_all (GTK_CONTAINER (dialog->list_boxes[i])); + } + + /* add new apps */ + related = gs_app_get_related (app); + for (guint i = 0; i < gs_app_list_length (related); i++) { + app_related = gs_app_list_index (related, i); + + section = get_app_section (app_related); + if (dialog->list_boxes[section] == NULL) + create_section (dialog, section); + + row = create_app_row (app_related); + gtk_list_box_insert (GTK_LIST_BOX (dialog->list_boxes[section]), row, -1); + } + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "os-update-list"); + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_NONE); + } else { + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "package-details"); + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_NONE); + } +} + +static void +button_back_cb (GtkWidget *widget, GsUpdateDialog *dialog) +{ + BackEntry *entry; + + /* return to the previous view */ + entry = g_queue_pop_head (dialog->back_entry_stack); + + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT); + gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), entry->stack_page); + gtk_stack_set_transition_type (GTK_STACK (dialog->stack), GTK_STACK_TRANSITION_TYPE_NONE); + + gtk_window_set_title (GTK_WINDOW (dialog), entry->title); + if (entry->focus) + gtk_widget_grab_focus (entry->focus); + back_entry_free (entry); + + gtk_widget_set_visible (dialog->button_back, !g_queue_is_empty (dialog->back_entry_stack)); +} + +static void +scrollbar_mapped_cb (GtkWidget *sb, GtkScrolledWindow *swin) +{ + GtkWidget *frame; + + frame = gtk_bin_get_child (GTK_BIN (gtk_bin_get_child (GTK_BIN (swin)))); + + if (gtk_widget_get_mapped (GTK_WIDGET (sb))) { + gtk_scrolled_window_set_shadow_type (swin, GTK_SHADOW_IN); + if (GTK_IS_FRAME (frame)) + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_NONE); + } else { + if (GTK_IS_FRAME (frame)) + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN); + gtk_scrolled_window_set_shadow_type (swin, GTK_SHADOW_NONE); + } +} + +static gboolean +key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer user_data) +{ + GsUpdateDialog *dialog = (GsUpdateDialog *) widget; + GdkKeymap *keymap; + GdkModifierType state; + gboolean is_rtl; + + if (!gtk_widget_is_visible (dialog->button_back) || !gtk_widget_is_sensitive (dialog->button_back)) + return GDK_EVENT_PROPAGATE; + + state = event->state; + keymap = gdk_keymap_get_for_display (gtk_widget_get_display (widget)); + gdk_keymap_add_virtual_modifiers (keymap, &state); + state = state & gtk_accelerator_get_default_mod_mask (); + is_rtl = gtk_widget_get_direction (dialog->button_back) == GTK_TEXT_DIR_RTL; + + if ((!is_rtl && state == GDK_MOD1_MASK && event->keyval == GDK_KEY_Left) || + (is_rtl && state == GDK_MOD1_MASK && event->keyval == GDK_KEY_Right) || + event->keyval == GDK_KEY_Back) { + gtk_widget_activate (dialog->button_back); + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +button_press_event (GsUpdateDialog *dialog, GdkEventButton *event) +{ + /* Mouse hardware back button is 8 */ + if (event->button != 8) + return GDK_EVENT_PROPAGATE; + + if (!gtk_widget_is_visible (dialog->button_back) || !gtk_widget_is_sensitive (dialog->button_back)) + return GDK_EVENT_PROPAGATE; + + gtk_widget_activate (dialog->button_back); + return GDK_EVENT_STOP; +} + +static void +set_plugin_loader (GsUpdateDialog *dialog, GsPluginLoader *plugin_loader) +{ + dialog->plugin_loader = g_object_ref (plugin_loader); +} + +static void +gs_update_dialog_dispose (GObject *object) +{ + GsUpdateDialog *dialog = GS_UPDATE_DIALOG (object); + + if (dialog->back_entry_stack != NULL) { + g_queue_free_full (dialog->back_entry_stack, (GDestroyNotify) back_entry_free); + dialog->back_entry_stack = NULL; + } + + g_cancellable_cancel (dialog->cancellable); + g_clear_object (&dialog->cancellable); + + g_clear_object (&dialog->plugin_loader); + + G_OBJECT_CLASS (gs_update_dialog_parent_class)->dispose (object); +} + +static void +gs_update_dialog_init (GsUpdateDialog *dialog) +{ + GtkWidget *scrollbar; + + gtk_widget_init_template (GTK_WIDGET (dialog)); + + dialog->back_entry_stack = g_queue_new (); + dialog->cancellable = g_cancellable_new (); + + g_signal_connect (GTK_LIST_BOX (dialog->list_box_installed_updates), "row-activated", + G_CALLBACK (installed_updates_row_activated_cb), dialog); + + g_signal_connect (dialog->button_back, "clicked", + G_CALLBACK (button_back_cb), + dialog); + + g_signal_connect_after (dialog, "show", G_CALLBACK (unset_focus), NULL); + + scrollbar = gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (dialog->scrolledwindow_details)); + g_signal_connect (scrollbar, "map", G_CALLBACK (scrollbar_mapped_cb), dialog->scrolledwindow_details); + g_signal_connect (scrollbar, "unmap", G_CALLBACK (scrollbar_mapped_cb), dialog->scrolledwindow_details); + + /* global keynav and mouse back button */ + g_signal_connect (dialog, "key-press-event", + G_CALLBACK (key_press_event), NULL); + g_signal_connect (dialog, "button-press-event", + G_CALLBACK (button_press_event), NULL); +} + +static void +gs_update_dialog_class_init (GsUpdateDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_update_dialog_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-update-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, box_header); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, button_back); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, image_icon); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, label_details); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, label_name); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, label_summary); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, list_box_installed_updates); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, os_update_description); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, os_update_box); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, scrolledwindow); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, scrolledwindow_details); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, spinner); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, stack); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, permissions_section_box); + gtk_widget_class_bind_template_child (widget_class, GsUpdateDialog, permissions_section_content); +} + +GtkWidget * +gs_update_dialog_new (GsPluginLoader *plugin_loader) +{ + GsUpdateDialog *dialog; + + dialog = g_object_new (GS_TYPE_UPDATE_DIALOG, + "use-header-bar", TRUE, + NULL); + set_plugin_loader (dialog, plugin_loader); + + return GTK_WIDGET (dialog); +} diff --git a/src/gs-update-dialog.h b/src/gs-update-dialog.h new file mode 100644 index 0000000..0008baa --- /dev/null +++ b/src/gs-update-dialog.h @@ -0,0 +1,27 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPDATE_DIALOG (gs_update_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsUpdateDialog, gs_update_dialog, GS, UPDATE_DIALOG, GtkDialog) + +GtkWidget *gs_update_dialog_new (GsPluginLoader *plugin_loader); +void gs_update_dialog_show_installed_updates (GsUpdateDialog *dialog); +void gs_update_dialog_show_update_details (GsUpdateDialog *dialog, + GsApp *app); + +G_END_DECLS diff --git a/src/gs-update-dialog.ui b/src/gs-update-dialog.ui new file mode 100644 index 0000000..a83b026 --- /dev/null +++ b/src/gs-update-dialog.ui @@ -0,0 +1,308 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsUpdateDialog" parent="GtkDialog"> + <property name="modal">True</property> + <property name="default_width">600</property> + <property name="default_height">600</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <child> + <object class="GtkButton" id="button_back"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <child internal-child="accessible"> + <object class="AtkObject" id="button_back_accessible"> + <property name="accessible-name" translatable="yes">Go back</property> + </object> + </child> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage" id="image_update_back"> + <property name="visible">True</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + <packing> + <property name="pack-type">start</property> + </packing> + </child> + </object> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="border_width">0</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="transition_duration">300</property> + <child> + <object class="GtkBox" id="box_spinner"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkSpinner" id="spinner"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + </child> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box_empty"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="spacing">16</property> + <property name="orientation">vertical</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="icon_empty"> + <property name="visible">True</property> + <property name="icon_name">org.gnome.Software-symbolic</property> + <property name="pixel-size">64</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_empty"> + <property name="visible">True</property> + <property name="justify">center</property> + <property name="label" translatable="yes">No updates have been installed on this system.</property> + <property name="wrap">True</property> + <property name="max_width_chars">40</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + </child> + </object> + <packing> + <property name="name">empty</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box7"> + <property name="visible">True</property> + <property name="margin_start">6</property> + <property name="margin_end">6</property> + <property name="margin_top">6</property> + <property name="margin_bottom">9</property> + <property name="border_width">5</property> + <property name="orientation">vertical</property> + <property name="spacing">9</property> + <child> + <object class="GtkBox" id="box_header"> + <property name="visible">True</property> + <property name="spacing">9</property> + <child> + <object class="GtkImage" id="image_icon"> + <property name="visible">True</property> + <property name="pixel_size">96</property> + <property name="icon_name">application-x-executable</property> + <property name="icon_size">0</property> + </object> + </child> + <child> + <object class="GtkBox" id="box9"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">3</property> + <child> + <object class="GtkLabel" id="label_name"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label">Inkscape</property> + <property name="selectable">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">50</property> + <property name="width_chars">50</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.3999999999999999"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label_summary"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label">Vector based drawing program</property> + <property name="selectable">True</property> + <property name="wrap">True</property> + <property name="max_width_chars">50</property> + <property name="width_chars">50</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="permissions_section_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="margin_top">12</property> + <property name="margin_bottom">18</property> + <child> + <object class="GtkLabel" id="permissions_section_title"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="halign">start</property> + <property name="margin_bottom">6</property> + <property name="label" translatable="yes">Requires additional permissions</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkBox" id="permissions_section_content"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_details"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkLabel" id="label_details"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="margin">6</property> + <property name="label">New in kmod 14-1 +* Moo +* bar</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">package-details</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin_top">24</property> + <property name="margin_bottom">18</property> + <property name="margin_start">18</property> + <property name="margin_end">18</property> + <child> + <object class="GtkLabel" id="os_update_description"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="wrap">True</property> + </object> + </child> + <child> + <object class="GtkBox" id="os_update_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="vexpand">False</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">os-update-list</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_installed_updates"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkFrame" id="frame_installed_updates"> + <property name="visible">True</property> + <property name="shadow_type">none</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <style> + <class name="view"/> + </style> + <child> + <object class="GsUpdateList" id="list_box_installed_updates"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">installed-updates-list</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup" id="sizegroup_update_details"> + <property name="ignore-hidden">False</property> + <property name="mode">horizontal</property> + <widgets> + <widget name="scrolledwindow"/> + <widget name="scrolledwindow_details"/> + </widgets> + </object> +</interface> diff --git a/src/gs-update-list.c b/src/gs-update-list.c new file mode 100644 index 0000000..5122329 --- /dev/null +++ b/src/gs-update-list.c @@ -0,0 +1,120 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-update-list.h" + +#include "gs-app-row.h" +#include "gs-common.h" + +typedef struct +{ + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; +} GsUpdateListPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsUpdateList, gs_update_list, GTK_TYPE_LIST_BOX) + +static void +gs_update_list_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + GsAppRow *app_row = GS_APP_ROW (user_data); + gs_app_row_unreveal (app_row); + } +} + +void +gs_update_list_add_app (GsUpdateList *update_list, GsApp *app) +{ + GsUpdateListPrivate *priv = gs_update_list_get_instance_private (update_list); + GtkWidget *app_row; + + app_row = gs_app_row_new (app); + gs_app_row_set_show_update (GS_APP_ROW (app_row), FALSE); + gs_app_row_set_show_buttons (GS_APP_ROW (app_row), FALSE); + gtk_container_add (GTK_CONTAINER (update_list), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + priv->sizegroup_image, + priv->sizegroup_name, + priv->sizegroup_desc, + NULL); + g_signal_connect_object (app, "notify::state", + G_CALLBACK (gs_update_list_app_state_notify_cb), + app_row, 0); + gtk_widget_show (app_row); +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header; + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static gint +list_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a)); + GsApp *b1 = gs_app_row_get_app (GS_APP_ROW (b)); + return g_strcmp0 (gs_app_get_name (a1), gs_app_get_name (b1)); +} + +static void +gs_update_list_dispose (GObject *object) +{ + GsUpdateList *update_list = GS_UPDATE_LIST (object); + GsUpdateListPrivate *priv = gs_update_list_get_instance_private (update_list); + + g_clear_object (&priv->sizegroup_image); + g_clear_object (&priv->sizegroup_name); + g_clear_object (&priv->sizegroup_desc); + + G_OBJECT_CLASS (gs_update_list_parent_class)->dispose (object); +} + +static void +gs_update_list_init (GsUpdateList *update_list) +{ + GsUpdateListPrivate *priv = gs_update_list_get_instance_private (update_list); + priv->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + priv->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + priv->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + gtk_list_box_set_header_func (GTK_LIST_BOX (update_list), + list_header_func, + update_list, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (update_list), + list_sort_func, + update_list, NULL); +} + +static void +gs_update_list_class_init (GsUpdateListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_update_list_dispose; +} + +GtkWidget * +gs_update_list_new (void) +{ + GsUpdateList *update_list; + update_list = g_object_new (GS_TYPE_UPDATE_LIST, NULL); + return GTK_WIDGET (update_list); +} diff --git a/src/gs-update-list.h b/src/gs-update-list.h new file mode 100644 index 0000000..731b412 --- /dev/null +++ b/src/gs-update-list.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPDATE_LIST (gs_update_list_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsUpdateList, gs_update_list, GS, UPDATE_LIST, GtkListBox) + +struct _GsUpdateListClass +{ + GtkListBoxClass parent_class; +}; + +GtkWidget *gs_update_list_new (void); +void gs_update_list_add_app (GsUpdateList *update_list, + GsApp *app); + +G_END_DECLS diff --git a/src/gs-update-monitor.c b/src/gs-update-monitor.c new file mode 100644 index 0000000..296a512 --- /dev/null +++ b/src/gs-update-monitor.c @@ -0,0 +1,1271 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> +#include <gsettings-desktop-schemas/gdesktop-enums.h> + +#include "gs-update-monitor.h" +#include "gs-common.h" + +#define SECONDS_IN_AN_HOUR (60 * 60) +#define SECONDS_IN_A_DAY (SECONDS_IN_AN_HOUR * 24) + +struct _GsUpdateMonitor { + GObject parent; + + GApplication *application; + GCancellable *cancellable; + GSettings *settings; + GsPluginLoader *plugin_loader; + GDBusProxy *proxy_upower; + GError *last_offline_error; + + GNetworkMonitor *network_monitor; + guint network_changed_handler; + GCancellable *network_cancellable; + + guint cleanup_notifications_id; /* at startup */ + guint check_startup_id; /* 60s after startup */ + guint check_hourly_id; /* and then every hour */ + guint check_daily_id; /* every 3rd day */ + guint notification_blocked_id; /* rate limit notifications */ +}; + +G_DEFINE_TYPE (GsUpdateMonitor, gs_update_monitor, G_TYPE_OBJECT) + +typedef struct { + GsUpdateMonitor *monitor; +} DownloadUpdatesData; + +static void +download_updates_data_free (DownloadUpdatesData *data) +{ + g_clear_object (&data->monitor); + g_slice_free (DownloadUpdatesData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(DownloadUpdatesData, download_updates_data_free); + +typedef struct { + GsUpdateMonitor *monitor; + GsApp *app; +} LanguagePackData; + +static void +language_pack_data_free (LanguagePackData *data) +{ + g_clear_object (&data->monitor); + g_clear_object (&data->app); + g_slice_free (LanguagePackData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(LanguagePackData, language_pack_data_free); + +static gboolean +reenable_offline_update_notification (gpointer data) +{ + GsUpdateMonitor *monitor = data; + monitor->notification_blocked_id = 0; + return G_SOURCE_REMOVE; +} + +static void +notify_offline_update_available (GsUpdateMonitor *monitor) +{ + const gchar *title; + const gchar *body; + guint64 elapsed_security = 0; + guint64 security_timestamp = 0; + g_autoptr(GNotification) n = NULL; + + if (gs_application_has_active_window (GS_APPLICATION (monitor->application))) + return; + if (monitor->notification_blocked_id > 0) + return; + + /* rate limit update notifications to once per hour */ + monitor->notification_blocked_id = g_timeout_add_seconds (SECONDS_IN_AN_HOUR, reenable_offline_update_notification, monitor); + + /* get time in days since we saw the first unapplied security update */ + g_settings_get (monitor->settings, + "security-timestamp", "x", &security_timestamp); + if (security_timestamp > 0) { + elapsed_security = (guint64) g_get_monotonic_time () - security_timestamp; + elapsed_security /= G_USEC_PER_SEC; + elapsed_security /= 60 * 60 * 24; + } + + /* only show the scary warning after the user has ignored + * security updates for a full day */ + if (elapsed_security > 1) { + title = _("Security Updates Pending"); + body = _("It is recommended that you install important updates now"); + n = g_notification_new (title); + g_notification_set_body (n, body); + g_notification_add_button (n, _("Restart & Install"), "app.reboot-and-install"); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + g_application_send_notification (monitor->application, "updates-available", n); + } else { + title = _("Software Updates Available"); + body = _("Important OS and application updates are ready to be installed"); + n = g_notification_new (title); + g_notification_set_body (n, body); + g_notification_add_button (n, _("Not Now"), "app.nop"); + g_notification_add_button_with_target (n, _("View"), "app.set-mode", "s", "updates"); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + g_application_send_notification (monitor->application, "updates-available", n); + } +} + +static gboolean +has_important_updates (GsAppList *apps) +{ + guint i; + GsApp *app; + + for (i = 0; i < gs_app_list_length (apps); i++) { + app = gs_app_list_index (apps, i); + if (gs_app_get_update_urgency (app) == AS_URGENCY_KIND_CRITICAL || + gs_app_get_update_urgency (app) == AS_URGENCY_KIND_HIGH) + return TRUE; + } + + return FALSE; +} + +static gboolean +check_if_timestamp_more_than_a_week_ago (GsUpdateMonitor *monitor, const gchar *timestamp) +{ + GTimeSpan d; + gint64 tmp; + g_autoptr(GDateTime) last_update = NULL; + g_autoptr(GDateTime) now = NULL; + + g_settings_get (monitor->settings, timestamp, "x", &tmp); + if (tmp == 0) + return TRUE; + + last_update = g_date_time_new_from_unix_local (tmp); + if (last_update == NULL) { + g_warning ("failed to set timestamp %" G_GINT64_FORMAT, tmp); + return TRUE; + } + + now = g_date_time_new_now_local (); + d = g_date_time_difference (now, last_update); + if (d >= 7 * G_TIME_SPAN_DAY) + return TRUE; + + return FALSE; +} + +static gboolean +no_updates_for_a_week (GsUpdateMonitor *monitor) +{ + if (check_if_timestamp_more_than_a_week_ago (monitor, "install-timestamp") || + check_if_timestamp_more_than_a_week_ago (monitor, "online-updates-timestamp")) + return TRUE; + + return FALSE; +} + +static gboolean +_filter_by_app_kind (GsApp *app, gpointer user_data) +{ + AsAppKind kind = GPOINTER_TO_UINT (user_data); + return gs_app_get_kind (app) == kind; +} + +static gboolean +_sort_by_rating_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + if (gs_app_get_rating (app1) < gs_app_get_rating (app2)) + return -1; + if (gs_app_get_rating (app1) > gs_app_get_rating (app2)) + return 1; + return 0; +} + +static GNotification * +_build_autoupdated_notification (GsUpdateMonitor *monitor, GsAppList *list) +{ + guint need_restart_cnt = 0; + g_autoptr(GsAppList) list_apps = NULL; + g_autoptr(GNotification) n = NULL; + g_autoptr(GString) body = g_string_new (NULL); + g_autofree gchar *title = NULL; + + /* filter out apps */ + list_apps = gs_app_list_copy (list); + gs_app_list_filter (list_apps, + _filter_by_app_kind, + GUINT_TO_POINTER(AS_APP_KIND_DESKTOP)); + gs_app_list_sort (list_apps, _sort_by_rating_cb, NULL); + /* FIXME: add the applications that are currently active that use one + * of the updated runtimes */ + if (gs_app_list_length (list_apps) == 0) { + g_debug ("no desktop apps in updated list, ignoring"); + return NULL; + } + + /* how many apps needs updating */ + for (guint i = 0; i < gs_app_list_length (list_apps); i++) { + GsApp *app = gs_app_list_index (list_apps, i); + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) + need_restart_cnt++; + } + + /* >1 app updated */ + if (gs_app_list_length (list_apps) > 0) { + if (need_restart_cnt > 0) { + /* TRANSLATORS: apps were auto-updated and restart is required */ + title = g_strdup_printf (ngettext ("%u Application Updated — Restart Required", + "%u Applications Updated — Restart Required", + gs_app_list_length (list_apps)), + gs_app_list_length (list_apps)); + } else { + /* TRANSLATORS: apps were auto-updated */ + title = g_strdup_printf (ngettext ("%u Application Updated", + "%u Applications Updated", + gs_app_list_length (list_apps)), + gs_app_list_length (list_apps)); + } + } + + /* 1 app updated */ + if (gs_app_list_length (list_apps) == 1) { + GsApp *app = gs_app_list_index (list_apps, 0); + /* TRANSLATORS: %1 is an application name, e.g. Firefox */ + g_string_append_printf (body, _("%s has been updated."), gs_app_get_name (app)); + if (need_restart_cnt > 0) { + /* TRANSLATORS: the app needs restarting */ + g_string_append_printf (body, " %s", _("Please restart the application.")); + } + + /* 2 apps updated */ + } else if (gs_app_list_length (list_apps) == 2) { + GsApp *app1 = gs_app_list_index (list_apps, 0); + GsApp *app2 = gs_app_list_index (list_apps, 1); + /* TRANSLATORS: %1 and %2 are both application names, e.g. Firefox */ + g_string_append_printf (body, _("%s and %s have been updated."), + gs_app_get_name (app1), + gs_app_get_name (app2)); + if (need_restart_cnt > 0) { + g_string_append (body, " "); + /* TRANSLATORS: at least one application needs restarting */ + g_string_append_printf (body, ngettext ("%u application requires a restart.", + "%u applications require a restart.", + need_restart_cnt), + need_restart_cnt); + } + + /* 3+ apps */ + } else if (gs_app_list_length (list_apps) >= 3) { + GsApp *app1 = gs_app_list_index (list_apps, 0); + GsApp *app2 = gs_app_list_index (list_apps, 1); + GsApp *app3 = gs_app_list_index (list_apps, 2); + /* TRANSLATORS: %1, %2 and %3 are all application names, e.g. Firefox */ + g_string_append_printf (body, _("Includes %s, %s and %s."), + gs_app_get_name (app1), + gs_app_get_name (app2), + gs_app_get_name (app3)); + if (need_restart_cnt > 0) { + g_string_append (body, " "); + /* TRANSLATORS: at least one application needs restarting */ + g_string_append_printf (body, ngettext ("%u application requires a restart.", + "%u applications require a restart.", + need_restart_cnt), + need_restart_cnt); + } + } + + /* create the notification */ + n = g_notification_new (title); + if (body->len > 0) + g_notification_set_body (n, body->str); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + return g_steal_pointer (&n); +} + +static void +update_finished_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get result */ + list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed update application: %s", error->message); + return; + } + + /* notifications are optional */ + if (g_settings_get_boolean (monitor->settings, "download-updates-notify")) { + g_autoptr(GNotification) n = NULL; + g_application_withdraw_notification (monitor->application, + "updates-installed"); + n = _build_autoupdated_notification (monitor, list); + if (n != NULL) + g_application_send_notification (monitor->application, + "updates-installed", n); + } +} + +static gboolean +_should_auto_update (GsApp *app) +{ + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE) + return FALSE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS)) + return FALSE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE)) + return FALSE; + return TRUE; +} + +static void +download_finished_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) update_online = NULL; + g_autoptr(GsAppList) update_offline = NULL; + + /* get result */ + list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get updates: %s", error->message); + return; + } + + update_online = gs_app_list_new (); + update_offline = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (_should_auto_update (app)) { + g_debug ("auto-updating %s", gs_app_get_unique_id (app)); + gs_app_list_add (update_online, app); + } else { + gs_app_list_add (update_offline, app); + } + } + + /* install any apps that can be installed LIVE */ + if (gs_app_list_length (update_online) > 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "list", update_online, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + update_finished_cb, + monitor); + } + + /* show a notification for offline updates */ + if (gs_app_list_length (update_offline) > 0) { + if (has_important_updates (update_offline) || + no_updates_for_a_week (monitor)) { + notify_offline_update_available (monitor); + } + } +} + +static void +get_updates_finished_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + g_autoptr(DownloadUpdatesData) download_updates_data = (DownloadUpdatesData *) data; + GsUpdateMonitor *monitor = download_updates_data->monitor; + guint64 security_timestamp = 0; + guint64 security_timestamp_old = 0; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) apps = NULL; + gboolean download_updates; + + /* get result */ + apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (apps == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get updates: %s", error->message); + return; + } + + /* no updates */ + if (gs_app_list_length (apps) == 0) { + g_debug ("no updates; withdrawing updates-available notification"); + g_application_withdraw_notification (monitor->application, + "updates-available"); + return; + } + + /* find security updates, or clear timestamp if there are now none */ + g_settings_get (monitor->settings, + "security-timestamp", "x", &security_timestamp_old); + for (guint i = 0; i < gs_app_list_length (apps); i++) { + GsApp *app = gs_app_list_index (apps, i); + if (gs_app_get_metadata_item (app, "is-security") != NULL) { + security_timestamp = (guint64) g_get_monotonic_time (); + break; + } + } + if (security_timestamp_old != security_timestamp) { + g_settings_set (monitor->settings, + "security-timestamp", "x", security_timestamp); + } + + g_debug ("got %u updates", gs_app_list_length (apps)); + +#ifdef HAVE_MOGWAI + download_updates = TRUE; +#else + download_updates = g_settings_get_boolean (monitor->settings, "download-updates"); +#endif + + if (download_updates) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* download any updates; individual plugins are responsible for deciding + * whether it’s appropriate to unconditionally download the updates, or + * to schedule the download in accordance with the user’s metered data + * preferences */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD, + "list", apps, + NULL); + g_debug ("Getting updates"); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + download_finished_cb, + monitor); + } else { + /* notify immediately if auto-updates are turned off */ + if (has_important_updates (apps) || + no_updates_for_a_week (monitor)) { + notify_offline_update_available (monitor); + } + } +} + +static gboolean +should_show_upgrade_notification (GsUpdateMonitor *monitor) +{ + GTimeSpan d; + gint64 tmp; + g_autoptr(GDateTime) now = NULL; + g_autoptr(GDateTime) then = NULL; + + g_settings_get (monitor->settings, "upgrade-notification-timestamp", "x", &tmp); + if (tmp == 0) + return TRUE; + then = g_date_time_new_from_unix_local (tmp); + if (then == NULL) { + g_warning ("failed to parse timestamp %" G_GINT64_FORMAT, tmp); + return TRUE; + } + + now = g_date_time_new_now_local (); + d = g_date_time_difference (now, then); + if (d >= 7 * G_TIME_SPAN_DAY) + return TRUE; + + return FALSE; +} + +static void +get_system_finished_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data); + g_autoptr(GError) error = NULL; + g_autoptr(GNotification) n = NULL; + g_autoptr(GsApp) app = NULL; + + /* get result */ + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get system: %s", error->message); + return; + } + + /* might be already showing, so just withdraw it and re-issue it */ + g_application_withdraw_notification (monitor->application, "eol"); + + /* do not show when the main window is active */ + if (gs_application_has_active_window (GS_APPLICATION (monitor->application))) + return; + + /* is not EOL */ + app = gs_plugin_loader_get_system_app (plugin_loader); + if (gs_app_get_state (app) != AS_APP_STATE_UNAVAILABLE) + return; + + /* TRANSLATORS: this is when the current OS version goes end-of-life */ + n = g_notification_new (_("Operating System Updates Unavailable")); + /* TRANSLATORS: this is the message dialog for the distro EOL notice */ + g_notification_set_body (n, _("Upgrade to continue receiving security updates.")); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "update"); + g_application_send_notification (monitor->application, "eol", n); +} + +static void +get_upgrades_finished_cb (GObject *object, + GAsyncResult *res, + gpointer data) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data); + GsApp *app; + g_autofree gchar *body = NULL; + g_autoptr(GDateTime) now = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GNotification) n = NULL; + g_autoptr(GsAppList) apps = NULL; + + /* get result */ + apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (apps == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_warning ("failed to get upgrades: %s", + error->message); + } + return; + } + + /* no results */ + if (gs_app_list_length (apps) == 0) { + g_debug ("no upgrades; withdrawing upgrades-available notification"); + g_application_withdraw_notification (monitor->application, + "upgrades-available"); + return; + } + + /* do not show if gnome-software is already open */ + if (gs_application_has_active_window (GS_APPLICATION (monitor->application))) + return; + + /* only nag about upgrades once per week */ + if (!should_show_upgrade_notification (monitor)) + return; + + g_debug ("showing distro upgrade notification"); + now = g_date_time_new_now_local (); + g_settings_set (monitor->settings, "upgrade-notification-timestamp", "x", + g_date_time_to_unix (now)); + + /* rely on the app list already being sorted with the + * chronologically newest release last */ + app = gs_app_list_index (apps, gs_app_list_length (apps) - 1); + + /* TRANSLATORS: this is a distro upgrade, the replacement would be the + * distro name, e.g. 'Fedora' */ + body = g_strdup_printf (_("A new version of %s is available to install"), + gs_app_get_name (app)); + + /* TRANSLATORS: this is a distro upgrade */ + n = g_notification_new (_("Software Upgrade Available")); + g_notification_set_body (n, body); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + g_application_send_notification (monitor->application, "upgrades-available", n); +} + +static void +get_updates (GsUpdateMonitor *monitor) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(DownloadUpdatesData) download_updates_data = NULL; + + /* disabled in gsettings or from a plugin */ + if (!gs_plugin_loader_get_allow_updates (monitor->plugin_loader)) { + g_debug ("not getting updates as not enabled"); + return; + } + + download_updates_data = g_slice_new0 (DownloadUpdatesData); + download_updates_data->monitor = g_object_ref (monitor); + + /* NOTE: this doesn't actually do any network access */ + g_debug ("Getting updates"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + get_updates_finished_cb, + g_steal_pointer (&download_updates_data)); +} + +void +gs_update_monitor_autoupdate (GsUpdateMonitor *monitor) +{ + get_updates (monitor); +} + +static void +get_upgrades (GsUpdateMonitor *monitor) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* disabled in gsettings or from a plugin */ + if (!gs_plugin_loader_get_allow_updates (monitor->plugin_loader)) { + g_debug ("not getting upgrades as not enabled"); + return; + } + + /* NOTE: this doesn't actually do any network access, it relies on the + * AppStream data being up to date, either by the appstream-data + * package being up-to-date, or the metadata being auto-downloaded */ + g_debug ("Getting upgrades"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + get_upgrades_finished_cb, + monitor); +} + +static void +get_system (GsUpdateMonitor *monitor) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + g_debug ("Getting system"); + app = gs_plugin_loader_get_system_app (monitor->plugin_loader); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, plugin_job, + monitor->cancellable, + get_system_finished_cb, + monitor); +} + +static void +refresh_cache_finished_cb (GObject *object, + GAsyncResult *res, + gpointer data) +{ + GsUpdateMonitor *monitor = data; + g_autoptr(GDateTime) now = NULL; + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (GS_PLUGIN_LOADER (object), res, &error)) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to refresh the cache: %s", error->message); + return; + } + + /* update the last checked timestamp */ + now = g_date_time_new_now_local (); + g_settings_set (monitor->settings, "check-timestamp", "x", + g_date_time_to_unix (now)); + + get_updates (monitor); +} + +typedef enum { + UP_DEVICE_LEVEL_UNKNOWN, + UP_DEVICE_LEVEL_NONE, + UP_DEVICE_LEVEL_DISCHARGING, + UP_DEVICE_LEVEL_LOW, + UP_DEVICE_LEVEL_CRITICAL, + UP_DEVICE_LEVEL_ACTION, + UP_DEVICE_LEVEL_LAST +} UpDeviceLevel; + +static void +install_language_pack_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(LanguagePackData) language_pack_data = data; + + if (!gs_plugin_loader_job_action_finish (GS_PLUGIN_LOADER (object), res, &error)) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_debug ("failed to install language pack: %s", error->message); + return; + } else { + g_debug ("language pack for %s installed", + gs_app_get_name (language_pack_data->app)); + } +} + +static void +get_language_pack_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data); + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) app_list = NULL; + + app_list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (app_list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_debug ("failed to find language pack: %s", error->message); + return; + } + + /* none found */ + if (gs_app_list_length (app_list) == 0) { + g_debug ("no language pack found"); + return; + } + + /* there should be one langpack for a given locale */ + app = g_object_ref (gs_app_list_index (app_list, 0)); + if (!gs_app_is_installed (app)) { + g_autoptr(LanguagePackData) language_pack_data = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + language_pack_data = g_slice_new0 (LanguagePackData); + language_pack_data->monitor = g_object_ref (monitor); + language_pack_data->app = g_object_ref (app); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + install_language_pack_cb, + g_steal_pointer (&language_pack_data)); + } +} + +/* + * determines active locale and looks for langpacks + * installs located language pack, if not already + */ +static void +check_language_pack (GsUpdateMonitor *monitor) { + + const gchar *locale; + g_autoptr(GsPluginJob) plugin_job = NULL; + + locale = gs_plugin_loader_get_locale (monitor->plugin_loader); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_LANGPACKS, + "search", locale, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + get_language_pack_cb, + monitor); +} + +static void +check_updates (GsUpdateMonitor *monitor) +{ + gint64 tmp; + gboolean refresh_on_metered; + g_autoptr(GDateTime) last_refreshed = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* never check for updates when offline */ + if (!gs_plugin_loader_get_network_available (monitor->plugin_loader)) + return; + + /* check for language pack */ + check_language_pack (monitor); + +#ifdef HAVE_MOGWAI + refresh_on_metered = TRUE; +#else + refresh_on_metered = g_settings_get_boolean (monitor->settings, + "refresh-when-metered"); +#endif + + if (!refresh_on_metered && + gs_plugin_loader_get_network_metered (monitor->plugin_loader)) + return; + + /* never refresh when the battery is low */ + if (monitor->proxy_upower != NULL) { + g_autoptr(GVariant) val = NULL; + val = g_dbus_proxy_get_cached_property (monitor->proxy_upower, + "WarningLevel"); + if (val != NULL) { + guint32 level = g_variant_get_uint32 (val); + if (level >= UP_DEVICE_LEVEL_LOW) { + g_debug ("not getting updates on low power"); + return; + } + } + } else { + g_debug ("no UPower support, so not doing power level checks"); + } + + g_settings_get (monitor->settings, "check-timestamp", "x", &tmp); + last_refreshed = g_date_time_new_from_unix_local (tmp); + if (last_refreshed != NULL) { + gint now_year, now_month, now_day, now_hour; + gint year, month, day; + g_autoptr(GDateTime) now = NULL; + + now = g_date_time_new_now_local (); + + g_date_time_get_ymd (now, &now_year, &now_month, &now_day); + now_hour = g_date_time_get_hour (now); + + g_date_time_get_ymd (last_refreshed, &year, &month, &day); + + /* check that it is the next day */ + if (!((now_year > year) || + (now_year == year && now_month > month) || + (now_year == year && now_month == month && now_day > day))) + return; + + /* ...and past 6am */ + if (!(now_hour >= 6)) + return; + } + + g_debug ("Daily update check due"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) (60 * 60 * 24), + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, plugin_job, + monitor->network_cancellable, + refresh_cache_finished_cb, + monitor); +} + +static gboolean +check_hourly_cb (gpointer data) +{ + GsUpdateMonitor *monitor = data; + + g_debug ("Hourly updates check"); + check_updates (monitor); + + return G_SOURCE_CONTINUE; +} + +static gboolean +check_thrice_daily_cb (gpointer data) +{ + GsUpdateMonitor *monitor = data; + + g_debug ("Daily upgrades check"); + get_upgrades (monitor); + get_system (monitor); + + return G_SOURCE_CONTINUE; +} + +static void +stop_upgrades_check (GsUpdateMonitor *monitor) +{ + if (monitor->check_daily_id == 0) + return; + + g_source_remove (monitor->check_daily_id); + monitor->check_daily_id = 0; +} + +static void +restart_upgrades_check (GsUpdateMonitor *monitor) +{ + stop_upgrades_check (monitor); + get_upgrades (monitor); + + monitor->check_daily_id = g_timeout_add_seconds (SECONDS_IN_A_DAY / 3, + check_thrice_daily_cb, + monitor); +} + +static void +stop_updates_check (GsUpdateMonitor *monitor) +{ + if (monitor->check_hourly_id == 0) + return; + + g_source_remove (monitor->check_hourly_id); + monitor->check_hourly_id = 0; +} + +static void +restart_updates_check (GsUpdateMonitor *monitor) +{ + stop_updates_check (monitor); + check_updates (monitor); + + monitor->check_hourly_id = g_timeout_add_seconds (SECONDS_IN_AN_HOUR, check_hourly_cb, + monitor); +} + +static gboolean +check_updates_on_startup_cb (gpointer data) +{ + GsUpdateMonitor *monitor = data; + + g_debug ("First hourly updates check"); + restart_updates_check (monitor); + + if (gs_plugin_loader_get_allow_updates (monitor->plugin_loader)) + restart_upgrades_check (monitor); + + monitor->check_startup_id = 0; + return G_SOURCE_REMOVE; +} + +static void +check_updates_upower_changed_cb (GDBusProxy *proxy, + GParamSpec *pspec, + GsUpdateMonitor *monitor) +{ + g_debug ("upower changed updates check"); + check_updates (monitor); +} + +static void +network_available_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsUpdateMonitor *monitor) +{ + check_updates (monitor); +} + +static void +get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data) +{ + GsUpdateMonitor *monitor = data; + GsApp *app; + const gchar *message; + const gchar *title; + guint64 time_last_notified; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) apps = NULL; + g_autoptr(GNotification) notification = NULL; + + /* get result */ + apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (apps == NULL) { + + /* save this in case the user clicks the + * 'Show Details' button from the notification below */ + g_clear_error (&monitor->last_offline_error); + monitor->last_offline_error = g_error_copy (error); + + /* TRANSLATORS: title when we offline updates have failed */ + notification = g_notification_new (_("Software Updates Failed")); + /* TRANSLATORS: message when we offline updates have failed */ + g_notification_set_body (notification, _("An important OS update failed to be installed.")); + g_notification_add_button (notification, _("Show Details"), "app.show-offline-update-error"); + g_notification_set_default_action (notification, "app.show-offline-update-error"); + g_application_send_notification (monitor->application, "offline-updates", notification); + return; + } + + /* no results */ + if (gs_app_list_length (apps) == 0) { + g_debug ("no historical updates; withdrawing notification"); + g_application_withdraw_notification (monitor->application, + "updates-available"); + return; + } + + /* have we notified about this before */ + app = gs_app_list_index (apps, 0); + g_settings_get (monitor->settings, + "install-timestamp", "x", &time_last_notified); + if (time_last_notified >= gs_app_get_install_date (app)) + return; + + if (gs_app_get_kind (app) == AS_APP_KIND_OS_UPGRADE) { + /* TRANSLATORS: Notification title when we've done a distro upgrade */ + notification = g_notification_new (_("System Upgrade Complete")); + + /* TRANSLATORS: This is the notification body when we've done a + * distro upgrade. First %s is the distro name and the 2nd %s + * is the version, e.g. "Welcome to Fedora 28!" */ + message = g_strdup_printf (_("Welcome to %s %s!"), + gs_app_get_name (app), + gs_app_get_version (app)); + g_notification_set_body (notification, message); + } else { + /* TRANSLATORS: title when we've done offline updates */ + title = ngettext ("Software Update Installed", + "Software Updates Installed", + gs_app_list_length (apps)); + /* TRANSLATORS: message when we've done offline updates */ + message = ngettext ("An important OS update has been installed.", + "Important OS updates have been installed.", + gs_app_list_length (apps)); + + notification = g_notification_new (title); + g_notification_set_body (notification, message); + /* TRANSLATORS: Button to look at the updates that were installed. + * Note that it has nothing to do with the application reviews, the + * users can't express their opinions here. In some languages + * "Review (evaluate) something" is a different translation than + * "Review (browse) something." */ + g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode", "s", "updated"); + g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated"); + } + g_application_send_notification (monitor->application, "offline-updates", notification); + + /* update the timestamp so we don't show again */ + g_settings_set (monitor->settings, + "install-timestamp", "x", gs_app_get_install_date (app)); + +} + +static gboolean +cleanup_notifications_cb (gpointer user_data) +{ + GsUpdateMonitor *monitor = user_data; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* this doesn't do any network access */ + g_debug ("getting historical updates for fresh session"); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, + NULL); + gs_plugin_loader_job_process_async (monitor->plugin_loader, + plugin_job, + monitor->cancellable, + get_updates_historical_cb, + monitor); + + /* wait until first check to show */ + g_application_withdraw_notification (monitor->application, + "updates-available"); + + monitor->cleanup_notifications_id = 0; + return G_SOURCE_REMOVE; +} + +void +gs_update_monitor_show_error (GsUpdateMonitor *monitor, GsShell *shell) +{ + const gchar *title; + const gchar *msg; + gboolean show_detailed_error; + + /* can this happen in reality? */ + if (monitor->last_offline_error == NULL) + return; + + /* TRANSLATORS: this is when the offline update failed */ + title = _("Failed To Update"); + + switch (monitor->last_offline_error->code) { + case GS_PLUGIN_ERROR_NOT_SUPPORTED: + /* TRANSLATORS: the user must have updated manually after + * the updates were prepared */ + msg = _("The system was already up to date."); + show_detailed_error = TRUE; + break; + case GS_PLUGIN_ERROR_CANCELLED: + /* TRANSLATORS: the user aborted the update manually */ + msg = _("The update was cancelled."); + show_detailed_error = FALSE; + break; + case GS_PLUGIN_ERROR_NO_NETWORK: + /* TRANSLATORS: the package manager needed to download + * something with no network available */ + msg = _("Internet access was required but wasn’t available. " + "Please make sure that you have internet access and try again."); + show_detailed_error = FALSE; + break; + case GS_PLUGIN_ERROR_NO_SECURITY: + /* TRANSLATORS: if the package is not signed correctly */ + msg = _("There were security issues with the update. " + "Please consult your software provider for more details."); + show_detailed_error = TRUE; + break; + case GS_PLUGIN_ERROR_NO_SPACE: + /* TRANSLATORS: we ran out of disk space */ + msg = _("There wasn’t enough disk space. Please free up some space and try again."); + show_detailed_error = FALSE; + break; + default: + /* TRANSLATORS: We didn't handle the error type */ + msg = _("We’re sorry: the update failed to install. " + "Please wait for another update and try again. " + "If the problem persists, contact your software provider."); + show_detailed_error = TRUE; + break; + } + + gs_utils_show_error_dialog (gs_shell_get_window (shell), + title, + msg, + show_detailed_error ? monitor->last_offline_error->message : NULL); +} + +static void +allow_updates_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsUpdateMonitor *monitor) +{ + if (gs_plugin_loader_get_allow_updates (plugin_loader)) { + /* We restart the updates check here to avoid the user + * potentially waiting for the hourly check */ + restart_updates_check (monitor); + restart_upgrades_check (monitor); + } else { + stop_upgrades_check (monitor); + } +} + +static void +gs_update_monitor_network_changed_cb (GNetworkMonitor *network_monitor, + gboolean available, + GsUpdateMonitor *monitor) +{ + /* cancel an on-going refresh if we're now in a metered connection */ + if (!g_settings_get_boolean (monitor->settings, "refresh-when-metered") && + g_network_monitor_get_network_metered (network_monitor)) { + g_cancellable_cancel (monitor->network_cancellable); + g_object_unref (monitor->network_cancellable); + monitor->network_cancellable = g_cancellable_new (); + } +} + +static void +gs_update_monitor_init (GsUpdateMonitor *monitor) +{ + GNetworkMonitor *network_monitor; + g_autoptr(GError) error = NULL; + monitor->settings = g_settings_new ("org.gnome.software"); + + /* cleanup at startup */ + monitor->cleanup_notifications_id = + g_idle_add (cleanup_notifications_cb, monitor); + + /* do a first check 60 seconds after login, and then every hour */ + monitor->check_startup_id = + g_timeout_add_seconds (60, check_updates_on_startup_cb, monitor); + + /* we use two cancellables because one can be cancelled by any network + * changes to a metered connection, and this shouldn't intervene with other + * operations */ + monitor->cancellable = g_cancellable_new (); + monitor->network_cancellable = g_cancellable_new (); + + /* connect to UPower to get the system power state */ + monitor->proxy_upower = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.UPower", + "/org/freedesktop/UPower/devices/DisplayDevice", + "org.freedesktop.UPower.Device", + NULL, + &error); + if (monitor->proxy_upower != NULL) { + g_signal_connect (monitor->proxy_upower, "notify", + G_CALLBACK (check_updates_upower_changed_cb), + monitor); + } else { + g_warning ("failed to connect to upower: %s", error->message); + } + + network_monitor = g_network_monitor_get_default (); + if (network_monitor == NULL) + return; + monitor->network_monitor = g_object_ref (network_monitor); + monitor->network_changed_handler = g_signal_connect (monitor->network_monitor, + "network-changed", + G_CALLBACK (gs_update_monitor_network_changed_cb), + monitor); +} + +static void +gs_update_monitor_dispose (GObject *object) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (object); + + if (monitor->network_changed_handler != 0) { + g_signal_handler_disconnect (monitor->network_monitor, + monitor->network_changed_handler); + monitor->network_changed_handler = 0; + } + + g_cancellable_cancel (monitor->cancellable); + g_clear_object (&monitor->cancellable); + g_cancellable_cancel (monitor->network_cancellable); + g_clear_object (&monitor->network_cancellable); + + stop_updates_check (monitor); + stop_upgrades_check (monitor); + + if (monitor->check_startup_id != 0) { + g_source_remove (monitor->check_startup_id); + monitor->check_startup_id = 0; + } + if (monitor->notification_blocked_id != 0) { + g_source_remove (monitor->notification_blocked_id); + monitor->notification_blocked_id = 0; + } + if (monitor->cleanup_notifications_id != 0) { + g_source_remove (monitor->cleanup_notifications_id); + monitor->cleanup_notifications_id = 0; + } + if (monitor->plugin_loader != NULL) { + g_signal_handlers_disconnect_by_func (monitor->plugin_loader, + network_available_notify_cb, + monitor); + monitor->plugin_loader = NULL; + } + g_clear_object (&monitor->settings); + g_clear_object (&monitor->proxy_upower); + + G_OBJECT_CLASS (gs_update_monitor_parent_class)->dispose (object); +} + +static void +gs_update_monitor_finalize (GObject *object) +{ + GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (object); + + g_application_release (monitor->application); + g_clear_error (&monitor->last_offline_error); + + G_OBJECT_CLASS (gs_update_monitor_parent_class)->finalize (object); +} + +static void +gs_update_monitor_class_init (GsUpdateMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_update_monitor_dispose; + object_class->finalize = gs_update_monitor_finalize; +} + +GsUpdateMonitor * +gs_update_monitor_new (GsApplication *application) +{ + GsUpdateMonitor *monitor; + + monitor = GS_UPDATE_MONITOR (g_object_new (GS_TYPE_UPDATE_MONITOR, NULL)); + monitor->application = G_APPLICATION (application); + g_application_hold (monitor->application); + + monitor->plugin_loader = gs_application_get_plugin_loader (application); + g_signal_connect (monitor->plugin_loader, "notify::allow-updates", + G_CALLBACK (allow_updates_notify_cb), monitor); + g_signal_connect (monitor->plugin_loader, "notify::network-available", + G_CALLBACK (network_available_notify_cb), monitor); + + return monitor; +} diff --git a/src/gs-update-monitor.h b/src/gs-update-monitor.h new file mode 100644 index 0000000..bee6c21 --- /dev/null +++ b/src/gs-update-monitor.h @@ -0,0 +1,28 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-application.h" +#include "gs-shell.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPDATE_MONITOR (gs_update_monitor_get_type ()) + +G_DECLARE_FINAL_TYPE (GsUpdateMonitor, gs_update_monitor, GS, UPDATE_MONITOR, GObject) + +GsUpdateMonitor *gs_update_monitor_new (GsApplication *app); +void gs_update_monitor_autoupdate (GsUpdateMonitor *monitor); +void gs_update_monitor_show_error (GsUpdateMonitor *monitor, + GsShell *shell); + +G_END_DECLS diff --git a/src/gs-updates-page.c b/src/gs-updates-page.c new file mode 100644 index 0000000..ee25448 --- /dev/null +++ b/src/gs-updates-page.c @@ -0,0 +1,1468 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "gs-shell.h" +#include "gs-updates-page.h" +#include "gs-common.h" +#include "gs-app-row.h" +#include "gs-plugin-private.h" +#include "gs-removal-dialog.h" +#include "gs-update-monitor.h" +#include "gs-updates-section.h" +#include "gs-upgrade-banner.h" +#include "gs-application.h" + +#ifdef HAVE_GNOME_DESKTOP +#include <gdesktop-enums.h> +#endif + +#include <langinfo.h> + +typedef enum { + GS_UPDATES_PAGE_FLAG_NONE = 0, + GS_UPDATES_PAGE_FLAG_HAS_UPDATES = 1 << 0, + GS_UPDATES_PAGE_FLAG_HAS_UPGRADES = 1 << 1, + GS_UPDATES_PAGE_FLAG_LAST +} GsUpdatesPageFlags; + +typedef enum { + GS_UPDATES_PAGE_STATE_STARTUP, + GS_UPDATES_PAGE_STATE_ACTION_REFRESH, + GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES, + GS_UPDATES_PAGE_STATE_MANAGED, + GS_UPDATES_PAGE_STATE_IDLE, + GS_UPDATES_PAGE_STATE_FAILED, + GS_UPDATES_PAGE_STATE_LAST, +} GsUpdatesPageState; + +struct _GsUpdatesPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GtkBuilder *builder; + GCancellable *cancellable; + GCancellable *cancellable_refresh; + GCancellable *cancellable_upgrade_download; + GSettings *settings; + GSettings *desktop_settings; + gboolean cache_valid; + guint action_cnt; + GsShell *shell; + GsPluginStatus last_status; + GsUpdatesPageState state; + GsUpdatesPageFlags result_flags; + GtkWidget *button_refresh; + GtkWidget *header_spinner_start; + GtkWidget *header_checking_label; + GtkWidget *header_start_box; + GtkWidget *header_end_box; + gboolean has_agreed_to_mobile_data; + gboolean ampm_available; + + GtkWidget *updates_box; + GtkWidget *button_updates_mobile; + GtkWidget *button_updates_offline; + GtkWidget *label_updates_failed; + GtkWidget *label_updates_last_checked; + GtkWidget *label_updates_spinner; + GtkWidget *scrolledwindow_updates; + GtkWidget *spinner_updates; + GtkWidget *stack_updates; + GtkWidget *upgrade_banner; + GtkWidget *box_end_of_life; + GtkWidget *label_end_of_life; + + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + GtkSizeGroup *sizegroup_header; + GtkListBox *sections[GS_UPDATES_SECTION_KIND_LAST]; +}; + +enum { + COLUMN_UPDATE_APP, + COLUMN_UPDATE_NAME, + COLUMN_UPDATE_VERSION, + COLUMN_UPDATE_LAST +}; + +G_DEFINE_TYPE (GsUpdatesPage, gs_updates_page, GS_TYPE_PAGE) + +static void +gs_updates_page_set_flag (GsUpdatesPage *self, GsUpdatesPageFlags flag) +{ + self->result_flags |= flag; +} + +static void +gs_updates_page_clear_flag (GsUpdatesPage *self, GsUpdatesPageFlags flag) +{ + self->result_flags &= ~flag; +} + +static const gchar * +gs_updates_page_state_to_string (GsUpdatesPageState state) +{ + if (state == GS_UPDATES_PAGE_STATE_STARTUP) + return "startup"; + if (state == GS_UPDATES_PAGE_STATE_ACTION_REFRESH) + return "action-refresh"; + if (state == GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES) + return "action-get-updates"; + if (state == GS_UPDATES_PAGE_STATE_MANAGED) + return "managed"; + if (state == GS_UPDATES_PAGE_STATE_IDLE) + return "idle"; + if (state == GS_UPDATES_PAGE_STATE_FAILED) + return "failed"; + return NULL; +} + +static void +gs_updates_page_invalidate (GsUpdatesPage *self) +{ + self->cache_valid = FALSE; +} + +static GsUpdatesSectionKind +_get_app_section (GsApp *app) +{ + if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE_LIVE) { + if (gs_app_get_kind (app) == AS_APP_KIND_FIRMWARE) + return GS_UPDATES_SECTION_KIND_ONLINE_FIRMWARE; + return GS_UPDATES_SECTION_KIND_ONLINE; + } + if (gs_app_get_kind (app) == AS_APP_KIND_FIRMWARE) + return GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE; + return GS_UPDATES_SECTION_KIND_OFFLINE; +} + +static GsAppList * +_get_all_apps (GsUpdatesPage *self) +{ + GsAppList *apps = gs_app_list_new (); + for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) { + GsAppList *list = gs_updates_section_get_list (GS_UPDATES_SECTION (self->sections[i])); + gs_app_list_add_list (apps, list); + } + return apps; +} + +static guint +_get_num_updates (GsUpdatesPage *self) +{ + guint count = 0; + g_autoptr(GsAppList) apps = _get_all_apps (self); + + for (guint i = 0; i < gs_app_list_length (apps); ++i) { + GsApp *app = gs_app_list_index (apps, i); + if (gs_app_is_updatable (app) || + gs_app_get_state (app) == AS_APP_STATE_INSTALLING) + ++count; + } + return count; +} + +static GDateTime * +time_next_midnight (void) +{ + GDateTime *next_midnight; + GTimeSpan since_midnight; + g_autoptr(GDateTime) now = NULL; + + now = g_date_time_new_now_local (); + since_midnight = g_date_time_get_hour (now) * G_TIME_SPAN_HOUR + + g_date_time_get_minute (now) * G_TIME_SPAN_MINUTE + + g_date_time_get_second (now) * G_TIME_SPAN_SECOND + + g_date_time_get_microsecond (now); + next_midnight = g_date_time_add (now, G_TIME_SPAN_DAY - since_midnight); + + return next_midnight; +} + +static gchar * +gs_updates_page_last_checked_time_string (GsUpdatesPage *self) +{ +#ifdef HAVE_GNOME_DESKTOP + GDesktopClockFormat clock_format; +#endif + const gchar *format_string; + gchar *time_string; + gboolean use_24h_time = FALSE; + gint64 tmp; + gint days_ago; + g_autoptr(GDateTime) last_checked = NULL; + g_autoptr(GDateTime) midnight = NULL; + + g_settings_get (self->settings, "check-timestamp", "x", &tmp); + if (tmp == 0) + return NULL; + last_checked = g_date_time_new_from_unix_local (tmp); + + midnight = time_next_midnight (); + days_ago = (gint) (g_date_time_difference (midnight, last_checked) / G_TIME_SPAN_DAY); + +#ifdef HAVE_GNOME_DESKTOP + clock_format = g_settings_get_enum (self->desktop_settings, "clock-format"); + use_24h_time = (clock_format == G_DESKTOP_CLOCK_FORMAT_24H || self->ampm_available == FALSE); +#endif + + if (days_ago < 1) { // today + if (use_24h_time) { + /* TRANSLATORS: Time in 24h format */ + format_string = _("%R"); + } else { + /* TRANSLATORS: Time in 12h format */ + format_string = _("%l:%M %p"); + } + } else if (days_ago < 2) { // yesterday + if (use_24h_time) { + /* TRANSLATORS: This is the word "Yesterday" followed by a + time string in 24h format. i.e. "Yesterday, 14:30" */ + format_string = _("Yesterday, %R"); + } else { + /* TRANSLATORS: This is the word "Yesterday" followed by a + time string in 12h format. i.e. "Yesterday, 2:30 PM" */ + format_string = _("Yesterday, %l:%M %p"); + } + } else if (days_ago < 3) { + format_string = _("Two days ago"); + } else if (days_ago < 4) { + format_string = _("Three days ago"); + } else if (days_ago < 5) { + format_string = _("Four days ago"); + } else if (days_ago < 6) { + format_string = _("Five days ago"); + } else if (days_ago < 7) { + format_string = _("Six days ago"); + } else if (days_ago < 8) { + format_string = _("One week ago"); + } else if (days_ago < 15) { + format_string = _("Two weeks ago"); + } else { + /* TRANSLATORS: This is the date string with: day number, month name, year. + i.e. "25 May 2012" */ + format_string = _("%e %B %Y"); + } + + time_string = g_date_time_format (last_checked, format_string); + + return time_string; +} + +static const gchar * +gs_updates_page_get_state_string (GsPluginStatus status) +{ + /* TRANSLATORS: the update panel is doing *something* vague */ + return _("Looking for new updates…"); +} + +static void +refresh_headerbar_updates_counter (GsUpdatesPage *self) +{ + GtkWidget *widget; + guint num_updates; + + num_updates = _get_num_updates (self); + + /* update the counter */ + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_updates_counter")); + if (num_updates > 0 && + gs_plugin_loader_get_allow_updates (self->plugin_loader)) { + g_autofree gchar *text = NULL; + text = g_strdup_printf ("%u", num_updates); + gtk_label_set_label (GTK_LABEL (widget), text); + gtk_widget_show (widget); + } else { + gtk_widget_hide (widget); + } + + /* update the tab style */ + if (num_updates > 0 && + gs_shell_get_mode (self->shell) != GS_SHELL_MODE_UPDATES) + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "needs-attention"); + else + gtk_style_context_remove_class (gtk_widget_get_style_context (widget), "needs-attention"); +} + +static void +gs_updates_page_update_ui_state (GsUpdatesPage *self) +{ + gboolean allow_mobile_refresh = TRUE; + g_autofree gchar *checked_str = NULL; + g_autofree gchar *spinner_str = NULL; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_UPDATES) + return; + + /* spinners */ + switch (self->state) { + case GS_UPDATES_PAGE_STATE_STARTUP: + case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES: + case GS_UPDATES_PAGE_STATE_ACTION_REFRESH: + /* if we have updates, avoid clearing the page with a spinner */ + if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) { + gs_stop_spinner (GTK_SPINNER (self->spinner_updates)); + gtk_spinner_start (GTK_SPINNER (self->header_spinner_start)); + gtk_widget_show (self->header_spinner_start); + gtk_widget_show (self->header_checking_label); + } else { + gs_start_spinner (GTK_SPINNER (self->spinner_updates)); + } + break; + default: + gs_stop_spinner (GTK_SPINNER (self->spinner_updates)); + gtk_spinner_stop (GTK_SPINNER (self->header_spinner_start)); + gtk_widget_hide (self->header_spinner_start); + gtk_widget_hide (self->header_checking_label); + break; + } + + /* spinner text */ + switch (self->state) { + case GS_UPDATES_PAGE_STATE_STARTUP: + spinner_str = g_strdup_printf ("%s\n%s", + /* TRANSLATORS: the updates panel is starting up */ + _("Setting up updates…"), + _("(This could take a while)")); + gtk_label_set_label (GTK_LABEL (self->label_updates_spinner), spinner_str); + break; + case GS_UPDATES_PAGE_STATE_ACTION_REFRESH: + spinner_str = g_strdup_printf ("%s\n%s", + gs_updates_page_get_state_string (self->last_status), + /* TRANSLATORS: the updates panel is starting up */ + _("(This could take a while)")); + gtk_label_set_label (GTK_LABEL (self->label_updates_spinner), spinner_str); + break; + default: + break; + } + + /* headerbar refresh icon */ + switch (self->state) { + case GS_UPDATES_PAGE_STATE_ACTION_REFRESH: + case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES: + gtk_image_set_from_icon_name (GTK_IMAGE (gtk_button_get_image (GTK_BUTTON (self->button_refresh))), + "media-playback-stop-symbolic", GTK_ICON_SIZE_MENU); + gtk_widget_show (self->button_refresh); + break; + case GS_UPDATES_PAGE_STATE_STARTUP: + case GS_UPDATES_PAGE_STATE_MANAGED: + gtk_widget_hide (self->button_refresh); + break; + case GS_UPDATES_PAGE_STATE_IDLE: + gtk_image_set_from_icon_name (GTK_IMAGE (gtk_button_get_image (GTK_BUTTON (self->button_refresh))), + "view-refresh-symbolic", GTK_ICON_SIZE_MENU); + if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) { + gtk_widget_show (self->button_refresh); + } else { + if (gs_plugin_loader_get_network_metered (self->plugin_loader) && + !self->has_agreed_to_mobile_data) + allow_mobile_refresh = FALSE; + gtk_widget_set_visible (self->button_refresh, allow_mobile_refresh); + } + break; + case GS_UPDATES_PAGE_STATE_FAILED: + gtk_image_set_from_icon_name (GTK_IMAGE (gtk_button_get_image (GTK_BUTTON (self->button_refresh))), + "view-refresh-symbolic", GTK_ICON_SIZE_MENU); + gtk_widget_show (self->button_refresh); + break; + default: + g_assert_not_reached (); + break; + } + gtk_widget_set_sensitive (self->button_refresh, + gs_plugin_loader_get_network_available (self->plugin_loader)); + + /* stack */ + switch (self->state) { + case GS_UPDATES_PAGE_STATE_MANAGED: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "managed"); + break; + case GS_UPDATES_PAGE_STATE_FAILED: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "failed"); + break; + case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES: + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), + "spinner"); + break; + case GS_UPDATES_PAGE_STATE_ACTION_REFRESH: + if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "view"); + } else { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "spinner"); + } + break; + case GS_UPDATES_PAGE_STATE_STARTUP: + case GS_UPDATES_PAGE_STATE_IDLE: + + /* if have updates, just show the view, otherwise show network */ + if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "view"); + break; + } + + /* check we have a "free" network connection */ + if (gs_plugin_loader_get_network_available (self->plugin_loader) && + !gs_plugin_loader_get_network_metered (self->plugin_loader)) { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "uptodate"); + break; + } + + /* expensive network connection */ + if (gs_plugin_loader_get_network_metered (self->plugin_loader)) { + if (self->has_agreed_to_mobile_data) { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "uptodate"); + } else { + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "mobile"); + } + break; + } + + /* no network connection */ + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "offline"); + break; + default: + g_assert_not_reached (); + break; + } + + /* any updates? */ + gtk_widget_set_visible (self->updates_box, + self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPDATES); + + /* last checked label */ + if (g_strcmp0 (gtk_stack_get_visible_child_name (GTK_STACK (self->stack_updates)), "uptodate") == 0) { + checked_str = gs_updates_page_last_checked_time_string (self); + if (checked_str != NULL) { + g_autofree gchar *last_checked = NULL; + + /* TRANSLATORS: This is the time when we last checked for updates */ + last_checked = g_strdup_printf (_("Last checked: %s"), checked_str); + gtk_label_set_label (GTK_LABEL (self->label_updates_last_checked), + last_checked); + } + gtk_widget_set_visible (self->label_updates_last_checked, checked_str != NULL); + } + + /* update the counter in headerbar */ + refresh_headerbar_updates_counter (self); +} + +static void +gs_updates_page_set_state (GsUpdatesPage *self, GsUpdatesPageState state) +{ + g_debug ("setting state from %s to %s (has-update:%i, has-upgrade:%i)", + gs_updates_page_state_to_string (self->state), + gs_updates_page_state_to_string (state), + (self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPDATES) > 0, + (self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPGRADES) > 0); + self->state = state; + gs_updates_page_update_ui_state (self); +} + +static void +gs_updates_page_decrement_refresh_count (GsUpdatesPage *self) +{ + /* every job increments this */ + if (self->action_cnt == 0) { + g_warning ("action_cnt already zero!"); + return; + } + if (--self->action_cnt > 0) + return; + + /* all done */ + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE); +} + +static void +gs_updates_page_network_available_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsUpdatesPage *self) +{ + gs_updates_page_update_ui_state (self); +} + +static void +gs_updates_page_get_updates_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsUpdatesPage *self) +{ + GtkWidget *widget; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + self->cache_valid = TRUE; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES); + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("updates-shell: failed to get updates: %s", error->message); + gtk_label_set_label (GTK_LABEL (self->label_updates_failed), + error->message); + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_FAILED); + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, + "button_updates_counter")); + gtk_widget_hide (widget); + return; + } + + /* add the results */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsUpdatesSectionKind section = _get_app_section (app); + gs_updates_section_add_app (GS_UPDATES_SECTION (self->sections[section]), app); + } + + /* update the counter in headerbar */ + refresh_headerbar_updates_counter (self); + + /* no results */ + if (gs_app_list_length (list) == 0) { + g_debug ("updates-shell: no updates to show"); + gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES); + } else { + gs_updates_page_set_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES); + } + + /* only when both set */ + gs_updates_page_decrement_refresh_count (self); +} + +static void +gs_updates_page_get_upgrades_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + GsUpdatesPage *self = GS_UPDATES_PAGE (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get the results */ + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES); + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_warning ("updates-shell: failed to get upgrades: %s", + error->message); + } + } else if (gs_app_list_length (list) == 0) { + g_debug ("updates-shell: no upgrades to show"); + gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES); + gtk_widget_set_visible (self->upgrade_banner, FALSE); + } else { + /* rely on the app list already being sorted with the + * chronologically newest release last */ + GsApp *app = gs_app_list_index (list, gs_app_list_length (list) - 1); + g_debug ("got upgrade %s", gs_app_get_id (app)); + gs_upgrade_banner_set_app (GS_UPGRADE_BANNER (self->upgrade_banner), app); + gs_updates_page_set_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES); + gtk_widget_set_visible (self->upgrade_banner, TRUE); + } + + /* only when both set */ + gs_updates_page_decrement_refresh_count (self); +} + +static void +gs_updates_page_get_system_finished_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + GsUpdatesPage *self = GS_UPDATES_PAGE (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GString) str = g_string_new (NULL); + + /* get result */ + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to get system: %s", error->message); + return; + } + + /* show or hide the end of life notification */ + app = gs_plugin_loader_get_system_app (plugin_loader); + if (app == NULL) { + g_warning ("failed to get system app"); + gtk_widget_set_visible (self->box_end_of_life, FALSE); + return; + } + if (gs_app_get_state (app) != AS_APP_STATE_UNAVAILABLE) { + gtk_widget_set_visible (self->box_end_of_life, FALSE); + return; + } + + /* construct a sufficiently scary message */ + if (gs_app_get_name (app) != NULL && gs_app_get_version (app) != NULL) { + /* TRANSLATORS: the first %s is the distro name, e.g. 'Fedora' + * and the second %s is the distro version, e.g. '25' */ + g_string_append_printf (str, _("%s %s is no longer supported."), + gs_app_get_name (app), + gs_app_get_version (app)); + } else { + /* TRANSLATORS: OS refers to operating system, e.g. Fedora */ + g_string_append (str, _("Your OS is no longer supported.")); + } + g_string_append (str, " "); + + /* TRANSLATORS: EOL distros do not get important updates */ + g_string_append (str, _("This means that it does not receive security updates.")); + g_string_append (str, " "); + + /* TRANSLATORS: upgrade refers to a major update, e.g. Fedora 25 to 26 */ + g_string_append (str, _("It is recommended that you upgrade to a more recent version.")); + + gtk_label_set_label (GTK_LABEL (self->label_end_of_life), str->str); + gtk_widget_set_visible (self->box_end_of_life, TRUE); + +} + +static void +gs_updates_page_load (GsUpdatesPage *self) +{ + guint64 refine_flags; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + if (self->action_cnt > 0) + return; + + /* remove all existing apps */ + for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) + gs_updates_section_remove_all (GS_UPDATES_SECTION (self->sections[i])); + + refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION; + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES); + self->action_cnt++; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "interactive", TRUE, + "refine-flags", refine_flags, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + (GAsyncReadyCallback) gs_updates_page_get_updates_cb, + self); + + /* get the system state */ + g_object_unref (plugin_job); + app = gs_plugin_loader_get_system_app (self->plugin_loader); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "interactive", TRUE, + "app", app, + "refine-flags", refine_flags, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_updates_page_get_system_finished_cb, + self); + + /* don't refresh every each time */ + if ((self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPGRADES) == 0) { + refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED; + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, + "interactive", TRUE, + "refine-flags", refine_flags, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, + plugin_job, + self->cancellable, + gs_updates_page_get_upgrades_cb, + self); + self->action_cnt++; + } +} + +static void +gs_updates_page_reload (GsPage *page) +{ + GsUpdatesPage *self = GS_UPDATES_PAGE (page); + + if (self->state == GS_UPDATES_PAGE_STATE_ACTION_REFRESH) { + g_debug ("ignoring reload as refresh is already in progress"); + return; + } + + gs_updates_page_invalidate (self); + gs_updates_page_load (self); +} + +static void +gs_updates_page_switch_to (GsPage *page, + gboolean scroll_up) +{ + GsUpdatesPage *self = GS_UPDATES_PAGE (page); + GtkWidget *widget; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_UPDATES) { + g_warning ("Called switch_to(updates) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "buttonbox_main")); + gtk_widget_show (widget); + widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "menu_button")); + gtk_widget_show (widget); + + gtk_widget_set_visible (self->button_refresh, TRUE); + + if (scroll_up) { + GtkAdjustment *adj; + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_updates)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + } + + /* no need to refresh */ + if (self->cache_valid) { + gs_updates_page_update_ui_state (self); + return; + } + + if (self->state == GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES) { + gs_updates_page_update_ui_state (self); + return; + } + gs_updates_page_load (self); +} + +static void +gs_updates_page_refresh_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsUpdatesPage *self) +{ + gboolean ret; + g_autoptr(GDateTime) now = NULL; + g_autoptr(GError) error = NULL; + + /* get the results */ + ret = gs_plugin_loader_job_action_finish (plugin_loader, res, &error); + if (!ret) { + /* user cancel */ + if (g_error_matches (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED)) { + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE); + return; + } + g_warning ("failed to refresh: %s", error->message); + gtk_label_set_label (GTK_LABEL (self->label_updates_failed), + error->message); + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_FAILED); + return; + } + + /* update the last checked timestamp */ + now = g_date_time_new_now_local (); + g_settings_set (self->settings, "check-timestamp", "x", + g_date_time_to_unix (now)); + + /* get the new list */ + gs_updates_page_invalidate (self); + gs_page_switch_to (GS_PAGE (self), TRUE); +} + +static void +gs_updates_page_get_new_updates (GsUpdatesPage *self) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* force a check for updates and download */ + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_ACTION_REFRESH); + + g_cancellable_cancel (self->cancellable_refresh); + g_clear_object (&self->cancellable_refresh); + self->cancellable_refresh = g_cancellable_new (); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "interactive", TRUE, + "age", (guint64) 1, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable_refresh, + (GAsyncReadyCallback) gs_updates_page_refresh_cb, + self); +} + +static void +gs_updates_page_show_network_settings (GsUpdatesPage *self) +{ + g_autoptr(GError) error = NULL; + if (!g_spawn_command_line_async ("gnome-control-center wifi", &error)) + g_warning ("Failed to open the control center: %s", error->message); +} + +static void +gs_updates_page_refresh_confirm_cb (GtkDialog *dialog, + GtkResponseType response_type, + GsUpdatesPage *self) +{ + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (dialog)); + + switch (response_type) { + case GTK_RESPONSE_REJECT: + /* open the control center */ + gs_updates_page_show_network_settings (self); + break; + case GTK_RESPONSE_ACCEPT: + self->has_agreed_to_mobile_data = TRUE; + gs_updates_page_get_new_updates (self); + break; + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_DELETE_EVENT: + break; + default: + g_assert_not_reached (); + } +} + +static void +gs_updates_page_button_network_settings_cb (GtkWidget *widget, + GsUpdatesPage *self) +{ + gs_updates_page_show_network_settings (self); +} + +static void +gs_updates_page_button_mobile_refresh_cb (GtkWidget *widget, + GsUpdatesPage *self) +{ + self->has_agreed_to_mobile_data = TRUE; + gs_updates_page_get_new_updates (self); +} + +static void +gs_updates_page_button_refresh_cb (GtkWidget *widget, + GsUpdatesPage *self) +{ + GtkWidget *dialog; + + /* cancel existing action? */ + if (self->state == GS_UPDATES_PAGE_STATE_ACTION_REFRESH) { + g_cancellable_cancel (self->cancellable_refresh); + g_clear_object (&self->cancellable_refresh); + return; + } + + /* check we have a "free" network connection */ + if (gs_plugin_loader_get_network_available (self->plugin_loader) && + !gs_plugin_loader_get_network_metered (self->plugin_loader)) { + gs_updates_page_get_new_updates (self); + + /* expensive network connection */ + } else if (gs_plugin_loader_get_network_available (self->plugin_loader) && + gs_plugin_loader_get_network_metered (self->plugin_loader)) { + if (self->has_agreed_to_mobile_data) { + gs_updates_page_get_new_updates (self); + return; + } + dialog = gtk_message_dialog_new (gs_shell_get_window (self->shell), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR | + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CANCEL, + /* TRANSLATORS: this is to explain that downloading updates may cost money */ + _("Charges May Apply")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + /* TRANSLATORS: we need network + * to do the updates check */ + _("Checking for updates while using mobile broadband could cause you to incur charges.")); + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: this is a link to the + * control-center network panel */ + _("Check _Anyway"), + GTK_RESPONSE_ACCEPT); + g_signal_connect (dialog, "response", + G_CALLBACK (gs_updates_page_refresh_confirm_cb), + self); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); + + /* no network connection */ + } else { + dialog = gtk_message_dialog_new (gs_shell_get_window (self->shell), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR | + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CANCEL, + /* TRANSLATORS: can't do updates check */ + _("No Network")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + /* TRANSLATORS: we need network + * to do the updates check */ + _("Internet access is required to check for updates.")); + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: this is a link to the + * control-center network panel */ + _("Network Settings"), + GTK_RESPONSE_REJECT); + g_signal_connect (dialog, "response", + G_CALLBACK (gs_updates_page_refresh_confirm_cb), + self); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); + } +} + +static void +gs_updates_page_pending_apps_changed_cb (GsPluginLoader *plugin_loader, + GsUpdatesPage *self) +{ + gs_updates_page_invalidate (self); +} + +typedef struct { + GsApp *app; + GsUpdatesPage *self; +} GsPageHelper; + +static void +gs_page_helper_free (GsPageHelper *helper) +{ + if (helper->app != NULL) + g_object_unref (helper->app); + if (helper->self != NULL) + g_object_unref (helper->self); + g_slice_free (GsPageHelper, helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPageHelper, gs_page_helper_free); + +static void +upgrade_download_finished_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + return; + g_warning ("failed to upgrade-download: %s", error->message); + } +} + +static void +gs_updates_page_upgrade_download_cb (GsUpgradeBanner *upgrade_banner, + GsUpdatesPage *self) +{ + GsApp *app; + GsPageHelper *helper; + g_autoptr(GsPluginJob) plugin_job = NULL; + + app = gs_upgrade_banner_get_app (upgrade_banner); + if (app == NULL) { + g_warning ("no upgrade available to download"); + return; + } + + helper = g_slice_new0 (GsPageHelper); + helper->app = g_object_ref (app); + helper->self = g_object_ref (self); + + if (self->cancellable_upgrade_download != NULL) + g_object_unref (self->cancellable_upgrade_download); + self->cancellable_upgrade_download = g_cancellable_new (); + g_debug ("Starting upgrade download with cancellable %p", self->cancellable_upgrade_download); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable_upgrade_download, + upgrade_download_finished_cb, + helper); +} + +static void +_cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsUpdatesPage *self = GS_UPDATES_PAGE (user_data); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (self->plugin_loader, res, &error)) { + g_warning ("failed to cancel trigger: %s", error->message); + return; + } +} + +static void +upgrade_reboot_failed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsUpdatesPage *self = (GsUpdatesPage *) user_data; + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GVariant) retval = NULL; + + /* get result */ + retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error); + if (retval != NULL) + return; + + if (error != NULL) { + g_warning ("Calling org.gnome.SessionManager.Reboot failed: %s", + error->message); + } + + app = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner)); + if (app == NULL) { + g_warning ("no upgrade to cancel"); + return; + } + + /* cancel trigger */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE_CANCEL, + "app", app, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + _cancel_trigger_failed_cb, + self); +} + +static void +upgrade_trigger_finished_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsUpdatesPage *self = (GsUpdatesPage *) user_data; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GError) error = NULL; + + /* get the results */ + if (!gs_plugin_loader_job_action_finish (self->plugin_loader, res, &error)) { + g_warning ("Failed to trigger offline update: %s", error->message); + return; + } + + /* trigger reboot */ + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, + upgrade_reboot_failed_cb, + self); +} + +static void +trigger_upgrade (GsUpdatesPage *self) +{ + GsApp *upgrade; + g_autoptr(GsPluginJob) plugin_job = NULL; + + upgrade = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner)); + if (upgrade == NULL) { + g_warning ("no upgrade available to install"); + return; + } + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_TRIGGER, + "interactive", TRUE, + "app", upgrade, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + upgrade_trigger_finished_cb, + self); +} + +static void +gs_updates_page_upgrade_confirm_cb (GtkDialog *dialog, + GtkResponseType response_type, + GsUpdatesPage *self) +{ + /* unmap the dialog */ + gtk_widget_destroy (GTK_WIDGET (dialog)); + + switch (response_type) { + case GTK_RESPONSE_ACCEPT: + g_debug ("agreed to upgrade removing apps"); + trigger_upgrade (self); + break; + case GTK_RESPONSE_CANCEL: + g_debug ("cancelled removal dialog"); + break; + case GTK_RESPONSE_DELETE_EVENT: + break; + default: + g_assert_not_reached (); + } +} + +static void +gs_updates_page_upgrade_install_cb (GsUpgradeBanner *upgrade_banner, + GsUpdatesPage *self) +{ + GsAppList *removals; + GsApp *upgrade; + GtkWidget *dialog; + guint cnt = 0; + guint i; + + upgrade = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner)); + if (upgrade == NULL) { + g_warning ("no upgrade available to install"); + return; + } + + /* count the removals */ + removals = gs_app_get_related (upgrade); + for (i = 0; i < gs_app_list_length (removals); i++) { + GsApp *app = gs_app_list_index (removals, i); + if (gs_app_get_state (app) != AS_APP_STATE_UNAVAILABLE) + continue; + cnt++; + } + + if (cnt == 0) { + /* no need for a removal confirmation dialog */ + trigger_upgrade (self); + return; + } + + dialog = gs_removal_dialog_new (); + g_signal_connect (dialog, "response", + G_CALLBACK (gs_updates_page_upgrade_confirm_cb), + self); + gs_removal_dialog_show_upgrade_removals (GS_REMOVAL_DIALOG (dialog), + upgrade); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); +} + +static void +gs_updates_page_upgrade_help_cb (GsUpgradeBanner *upgrade_banner, + GsUpdatesPage *self) +{ + GsApp *app; + const gchar *uri; + + app = gs_upgrade_banner_get_app (upgrade_banner); + if (app == NULL) { + g_warning ("no upgrade available to launch"); + return; + } + + /* open the link */ + uri = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE); + gs_shell_show_uri (self->shell, uri); +} + +static void +gs_updates_page_invalidate_downloaded_upgrade (GsUpdatesPage *self) +{ + GsApp *app; + app = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner)); + if (app == NULL) + return; + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE) + return; + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + g_debug ("resetting %s to AVAILABLE as the updates have changed", + gs_app_get_id (app)); +} + +static gboolean +gs_shell_update_are_updates_in_progress (GsUpdatesPage *self) +{ + g_autoptr(GsAppList) list = _get_all_apps (self); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + switch (gs_app_get_state (app)) { + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + return TRUE; + break; + default: + break; + } + } + return FALSE; +} + +static void +gs_updates_page_changed_cb (GsPluginLoader *plugin_loader, + GsUpdatesPage *self) +{ + /* if we do a live update and the upgrade is waiting to be deployed + * then make sure all new packages are downloaded */ + gs_updates_page_invalidate_downloaded_upgrade (self); + + /* check to see if any apps in the app list are in a processing state */ + if (gs_shell_update_are_updates_in_progress (self)) { + g_debug ("ignoring updates-changed as updates in progress"); + return; + } + + /* refresh updates list */ + gs_updates_page_reload (GS_PAGE (self)); +} + +static void +gs_updates_page_status_changed_cb (GsPluginLoader *plugin_loader, + GsApp *app, + GsPluginStatus status, + GsUpdatesPage *self) +{ + switch (status) { + case GS_PLUGIN_STATUS_INSTALLING: + case GS_PLUGIN_STATUS_REMOVING: + if (app == NULL || + (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE && + gs_app_get_id (app) != NULL)) { + /* if we do a install or remove then make sure all new + * packages are downloaded */ + gs_updates_page_invalidate_downloaded_upgrade (self); + } + break; + default: + break; + } + + self->last_status = status; + gs_updates_page_update_ui_state (self); +} + +static void +gs_updates_page_allow_updates_notify_cb (GsPluginLoader *plugin_loader, + GParamSpec *pspec, + GsUpdatesPage *self) +{ + if (!gs_plugin_loader_get_allow_updates (plugin_loader)) { + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_MANAGED); + return; + } + gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE); +} + +static void +gs_updates_page_upgrade_cancel_cb (GsUpgradeBanner *upgrade_banner, + GsUpdatesPage *self) +{ + g_debug ("Cancelling upgrade download with %p", self->cancellable_upgrade_download); + g_cancellable_cancel (self->cancellable_upgrade_download); +} + +static gboolean +gs_updates_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GtkBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + GsUpdatesPage *self = GS_UPDATES_PAGE (page); + AtkObject *accessible; + + g_return_val_if_fail (GS_IS_UPDATES_PAGE (self), TRUE); + + for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) { + self->sections[i] = gs_updates_section_new (i, plugin_loader, page); + gs_updates_section_set_size_groups (GS_UPDATES_SECTION (self->sections[i]), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button, + self->sizegroup_header); + gtk_widget_set_vexpand (GTK_WIDGET (self->sections[i]), FALSE); + gtk_container_add (GTK_CONTAINER (self->updates_box), GTK_WIDGET (self->sections[i])); + } + + self->shell = shell; + + self->plugin_loader = g_object_ref (plugin_loader); + g_signal_connect (self->plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_updates_page_pending_apps_changed_cb), + self); + g_signal_connect (self->plugin_loader, "status-changed", + G_CALLBACK (gs_updates_page_status_changed_cb), + self); + g_signal_connect (self->plugin_loader, "updates-changed", + G_CALLBACK (gs_updates_page_changed_cb), + self); + g_signal_connect_object (self->plugin_loader, "notify::allow-updates", + G_CALLBACK (gs_updates_page_allow_updates_notify_cb), + self, 0); + g_signal_connect_object (self->plugin_loader, "notify::network-available", + G_CALLBACK (gs_updates_page_network_available_notify_cb), + self, 0); + self->builder = g_object_ref (builder); + self->cancellable = g_object_ref (cancellable); + + /* setup system upgrades */ + g_signal_connect (self->upgrade_banner, "download-clicked", + G_CALLBACK (gs_updates_page_upgrade_download_cb), self); + g_signal_connect (self->upgrade_banner, "install-clicked", + G_CALLBACK (gs_updates_page_upgrade_install_cb), self); + g_signal_connect (self->upgrade_banner, "cancel-clicked", + G_CALLBACK (gs_updates_page_upgrade_cancel_cb), self); + g_signal_connect (self->upgrade_banner, "help-clicked", + G_CALLBACK (gs_updates_page_upgrade_help_cb), self); + + self->header_end_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_visible (self->header_end_box, TRUE); + gs_page_set_header_end_widget (GS_PAGE (self), self->header_end_box); + + self->header_start_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_visible (self->header_start_box, TRUE); + gs_page_set_header_start_widget (GS_PAGE (self), self->header_start_box); + + /* This label indicates that the update check is in progress */ + self->header_checking_label = gtk_label_new (_("Checking…")); + gtk_container_add (GTK_CONTAINER (self->header_start_box), self->header_checking_label); + gtk_container_child_set(GTK_CONTAINER (self->header_start_box), self->header_checking_label, + "pack-type", GTK_PACK_END, NULL); + self->header_spinner_start = gtk_spinner_new (); + gtk_container_add (GTK_CONTAINER (self->header_start_box), self->header_spinner_start); + gtk_container_child_set (GTK_CONTAINER (self->header_start_box), self->header_spinner_start, + "pack-type", GTK_PACK_END, NULL); + + /* setup update details window */ + self->button_refresh = gtk_button_new_from_icon_name ("view-refresh-symbolic", GTK_ICON_SIZE_MENU); + accessible = gtk_widget_get_accessible (self->button_refresh); + if (accessible != NULL) + atk_object_set_name (accessible, _("Check for updates")); + gtk_container_add (GTK_CONTAINER (self->header_start_box), self->button_refresh); + g_signal_connect (self->button_refresh, "clicked", + G_CALLBACK (gs_updates_page_button_refresh_cb), + self); + + g_signal_connect (self->button_updates_mobile, "clicked", + G_CALLBACK (gs_updates_page_button_mobile_refresh_cb), + self); + g_signal_connect (self->button_updates_offline, "clicked", + G_CALLBACK (gs_updates_page_button_network_settings_cb), + self); + + /* visually aligned */ + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_header = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + /* set initial state */ + if (!gs_plugin_loader_get_allow_updates (self->plugin_loader)) + self->state = GS_UPDATES_PAGE_STATE_MANAGED; + return TRUE; +} + +static void +gs_updates_page_dispose (GObject *object) +{ + GsUpdatesPage *self = GS_UPDATES_PAGE (object); + + g_cancellable_cancel (self->cancellable_refresh); + g_clear_object (&self->cancellable_refresh); + g_cancellable_cancel (self->cancellable_upgrade_download); + g_clear_object (&self->cancellable_upgrade_download); + + for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) { + if (self->sections[i] != NULL) { + gtk_widget_destroy (GTK_WIDGET (self->sections[i])); + self->sections[i] = NULL; + } + } + + g_clear_object (&self->builder); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + g_clear_object (&self->settings); + g_clear_object (&self->desktop_settings); + + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + g_clear_object (&self->sizegroup_header); + + G_OBJECT_CLASS (gs_updates_page_parent_class)->dispose (object); +} + +static void +gs_updates_page_class_init (GsUpdatesPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_updates_page_dispose; + page_class->switch_to = gs_updates_page_switch_to; + page_class->reload = gs_updates_page_reload; + page_class->setup = gs_updates_page_setup; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-updates-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, updates_box); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_updates_mobile); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_updates_offline); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, label_updates_failed); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, label_updates_last_checked); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, label_updates_spinner); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, scrolledwindow_updates); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, spinner_updates); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, stack_updates); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, upgrade_banner); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, box_end_of_life); + gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, label_end_of_life); +} + +static void +gs_updates_page_init (GsUpdatesPage *self) +{ + const char *ampm; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->state = GS_UPDATES_PAGE_STATE_STARTUP; + self->settings = g_settings_new ("org.gnome.software"); + self->desktop_settings = g_settings_new ("org.gnome.desktop.interface"); + + self->sizegroup_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_header = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + ampm = nl_langinfo (AM_STR); + if (ampm != NULL && *ampm != '\0') + self->ampm_available = TRUE; +} + +GsUpdatesPage * +gs_updates_page_new (void) +{ + GsUpdatesPage *self; + self = g_object_new (GS_TYPE_UPDATES_PAGE, NULL); + return GS_UPDATES_PAGE (self); +} diff --git a/src/gs-updates-page.h b/src/gs-updates-page.h new file mode 100644 index 0000000..761c834 --- /dev/null +++ b/src/gs-updates-page.h @@ -0,0 +1,22 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPDATES_PAGE (gs_updates_page_get_type ()) + +G_DECLARE_FINAL_TYPE (GsUpdatesPage, gs_updates_page, GS, UPDATES_PAGE, GsPage) + +GsUpdatesPage *gs_updates_page_new (void); + +G_END_DECLS diff --git a/src/gs-updates-page.ui b/src/gs-updates-page.ui new file mode 100644 index 0000000..64bbe68 --- /dev/null +++ b/src/gs-updates-page.ui @@ -0,0 +1,373 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="GsUpdatesPage" parent="GsPage"> + <child internal-child="accessible"> + <object class="AtkObject" id="updates-accessible"> + <property name="accessible-name" translatable="yes">Updates page</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="box_end_of_life"> + <property name="visible">False</property> + <property name="border_width">0</property> + <property name="orientation">horizontal</property> + <property name="spacing">18</property> + <style> + <class name="eol-box"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="pixel_size">16</property> + <property name="icon_name">dialog-warning-symbolic</property> + <property name="margin_top">18</property> + <property name="margin_left">18</property> + <property name="valign">start</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <property name="visible">True</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="margin_right">18</property> + <property name="margin_top">18</property> + <property name="label" translatable="yes">Operating System Updates Unavailable</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label_end_of_life"> + <property name="visible">True</property> + <property name="margin_right">18</property> + <property name="margin_bottom">18</property> + <property name="label">Your OS is no longer supported. This means that it does not receive security updates. It is recommended that you upgrade to a more recent version.</property> + <property name="wrap">True</property> + <property name="width_chars">80</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="stack_updates"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="updates_spinner_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkSpinner" id="spinner_updates"> + <property name="visible">True</property> + <property name="width_request">32</property> + <property name="height_request">32</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_updates_spinner"> + <property name="visible">True</property> + <property name="label"/> + <property name="justify">center</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="name">spinner</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow_updates"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">none</property> + <child> + <object class="GsFixedSizeBin" id="gs_fixed_bin"> + <property name="visible">True</property> + <property name="preferred-width">860</property> + <child> + <object class="GtkBox" id="list_box_updates_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GsUpgradeBanner" id="upgrade_banner"> + <property name="visible">False</property> + <property name="hexpand">True</property> + <property name="vexpand">False</property> + <property name="margin_top">18</property> + </object> + </child> + <child> + <object class="GtkBox" id="updates_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="margin_bottom">18</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">view</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">48</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GsUpgradeBanner" id="upgrade_banner_uptodate"> + <property name="visible">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + </object> + </child> + <child type="center"> + <object class="GtkBox" id="updates_uptodate_centerbox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_updates"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">object-select-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="label" translatable="yes" comments="TRANSLATORS: This means all software (plural) installed on this system is up to date.">Software is up to date</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="label_updates_last_checked"> + <property name="visible">True</property> + <property name="margin_bottom">32</property> + <property name="label">Last checked: HH:MM</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + <packing> + <property name="name">uptodate</property> + </packing> + </child> + <child> + <object class="GtkBox" id="updates_mobile_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_updates_mobile"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">dialog-warning-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_updates_mobile"> + <property name="visible">True</property> + <property name="label" translatable="yes">Checking for updates when using mobile broadband could cause you to incur charges</property> + <property name="wrap">True</property> + <property name="halign">center</property> + <property name="max-width-chars">40</property> + <property name="justify">center</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton" id="button_updates_mobile"> + <property name="label" translatable="yes">_Check Anyway</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="hexpand">False</property> + <property name="halign">center</property> + </object> + </child> + </object> + <packing> + <property name="name">mobile</property> + </packing> + </child> + <child> + <object class="GtkBox" id="updates_offline_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_updates_offline"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">network-offline-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_updates_offline"> + <property name="visible">True</property> + <property name="label" translatable="yes">Go online to check for updates</property> + <property name="wrap">True</property> + <property name="halign">center</property> + <property name="justify">center</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton" id="button_updates_offline"> + <property name="label" translatable="yes">_Network Settings</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="hexpand">False</property> + <property name="halign">center</property> + </object> + </child> + </object> + <packing> + <property name="name">offline</property> + </packing> + </child> + <child> + <object class="GtkBox" id="updates_failed_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage" id="image_updates_failed"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">action-unavailable-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label_updates_failed"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <property name="label" translatable="No">Failed to get updates</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="name">failed</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="pixel_size">128</property> + <property name="icon_name">action-unavailable-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <property name="label" translatable="yes">Updates are automatically managed</property> + <attributes> + <attribute name="scale" value="1.4"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="name">managed</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-updates-section.c b/src/gs-updates-section.c new file mode 100644 index 0000000..efab7d6 --- /dev/null +++ b/src/gs-updates-section.c @@ -0,0 +1,642 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "gs-app-list-private.h" +#include "gs-app-row.h" +#include "gs-page.h" +#include "gs-common.h" +#include "gs-progress-button.h" +#include "gs-update-dialog.h" +#include "gs-updates-section.h" +#include "gs-utils.h" + +struct _GsUpdatesSection +{ + GtkListBox parent_instance; + GsAppList *list; + GsUpdatesSectionKind kind; + GCancellable *cancellable; + GsPage *page; + GsPluginLoader *plugin_loader; + GtkSizeGroup *sizegroup_image; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_desc; + GtkSizeGroup *sizegroup_button; + GtkSizeGroup *sizegroup_header; + GtkWidget *button_download; + GtkWidget *button_update; + GtkWidget *button_cancel; + GtkStack *button_stack; + GtkWidget *section_header; +}; + +G_DEFINE_TYPE (GsUpdatesSection, gs_updates_section, GTK_TYPE_LIST_BOX) + +GsAppList * +gs_updates_section_get_list (GsUpdatesSection *self) +{ + return self->list; +} + +static void +_app_row_button_clicked_cb (GsAppRow *app_row, GsUpdatesSection *self) +{ + GsApp *app = gs_app_row_get_app (app_row); + if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE) + return; + gs_page_update_app (GS_PAGE (self->page), app, gs_app_get_cancellable (app)); +} + +static void +_row_unrevealed_cb (GObject *row, GParamSpec *pspec, gpointer data) +{ + GtkWidget *list; + + list = gtk_widget_get_parent (GTK_WIDGET (row)); + if (list == NULL) + return; + gtk_container_remove (GTK_CONTAINER (list), GTK_WIDGET (row)); +} + +static void +_unreveal_row (GsAppRow *app_row) +{ + gs_app_row_unreveal (app_row); + g_signal_connect (app_row, "unrevealed", + G_CALLBACK (_row_unrevealed_cb), NULL); +} + +static void +_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED) { + GsAppRow *app_row = GS_APP_ROW (user_data); + _unreveal_row (app_row); + } +} + +void +gs_updates_section_add_app (GsUpdatesSection *self, GsApp *app) +{ + GtkWidget *app_row; + app_row = gs_app_row_new (app); + gs_app_row_set_show_update (GS_APP_ROW (app_row), TRUE); + gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE); + g_signal_connect (app_row, "button-clicked", + G_CALLBACK (_app_row_button_clicked_cb), + self); + gtk_container_add (GTK_CONTAINER (self), app_row); + gs_app_list_add (self->list, app); + + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_image, + self->sizegroup_name, + self->sizegroup_desc, + self->sizegroup_button); + g_signal_connect_object (app, "notify::state", + G_CALLBACK (_app_state_notify_cb), + app_row, 0); + gtk_widget_show (GTK_WIDGET (self)); +} + +void +gs_updates_section_remove_all (GsUpdatesSection *self) +{ + g_autoptr(GList) children = NULL; + children = gtk_container_get_children (GTK_CONTAINER (self)); + for (GList *l = children; l != NULL; l = l->next) { + GtkWidget *w = GTK_WIDGET (l->data); + gtk_container_remove (GTK_CONTAINER (self), w); + } + gs_app_list_remove_all (self->list); + gtk_widget_hide (GTK_WIDGET (self)); +} + +typedef struct { + GsUpdatesSection *self; + gboolean do_reboot; + gboolean do_reboot_notification; +} GsUpdatesSectionUpdateHelper; + +static gchar * +_get_app_sort_key (GsApp *app) +{ + GString *key; + g_autofree gchar *sort_name = NULL; + + key = g_string_sized_new (64); + + /* sort apps by kind */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_OS_UPDATE: + g_string_append (key, "1:"); + break; + case AS_APP_KIND_DESKTOP: + g_string_append (key, "2:"); + break; + case AS_APP_KIND_WEB_APP: + g_string_append (key, "3:"); + break; + case AS_APP_KIND_RUNTIME: + g_string_append (key, "4:"); + break; + case AS_APP_KIND_ADDON: + g_string_append (key, "5:"); + break; + case AS_APP_KIND_CODEC: + g_string_append (key, "6:"); + break; + case AS_APP_KIND_FONT: + g_string_append (key, "6:"); + break; + case AS_APP_KIND_INPUT_METHOD: + g_string_append (key, "7:"); + break; + case AS_APP_KIND_SHELL_EXTENSION: + g_string_append (key, "8:"); + break; + default: + g_string_append (key, "9:"); + break; + } + + /* finally, sort by short name */ + if (gs_app_get_name (app) != NULL) { + sort_name = gs_utils_sort_key (gs_app_get_name (app)); + g_string_append (key, sort_name); + } + + return g_string_free (key, FALSE); +} + +static gint +_list_sort_func (GtkListBoxRow *a, GtkListBoxRow *b, gpointer user_data) +{ + GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a)); + GsApp *a2 = gs_app_row_get_app (GS_APP_ROW (b)); + g_autofree gchar *key1 = _get_app_sort_key (a1); + g_autofree gchar *key2 = _get_app_sort_key (a2); + + /* compare the keys according to the algorithm above */ + return g_strcmp0 (key1, key2); +} + +static void +_update_helper_free (GsUpdatesSectionUpdateHelper *helper) +{ + g_object_unref (helper->self); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsUpdatesSectionUpdateHelper, _update_helper_free); + +static void +_cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsUpdatesSection *self = GS_UPDATES_SECTION (user_data); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (self->plugin_loader, res, &error)) { + g_warning ("failed to cancel trigger: %s", error->message); + return; + } +} + +static void +_reboot_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsUpdatesSection *self = GS_UPDATES_SECTION (user_data); + g_autoptr(GError) error = NULL; + GsApp *app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GVariant) retval = NULL; + + /* get result */ + retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error); + if (retval != NULL) + return; + + if (error != NULL) { + g_warning ("Calling org.gnome.SessionManager.Reboot failed: %s", + error->message); + } + + /* cancel trigger */ + app = gs_app_list_index (self->list, 0); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE_CANCEL, + "app", app, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + gs_app_get_cancellable (app), + _cancel_trigger_failed_cb, + self); +} + +static gboolean +_all_offline_updates_downloaded (GsUpdatesSection *self) +{ + /* use the download size to figure out what is downloaded and what not */ + for (guint i = 0; i < gs_app_list_length (self->list); i++) { + GsApp *app = gs_app_list_index (self->list, i); + guint64 size = gs_app_get_size_download (app); + if (size != 0) + return FALSE; + } + + return TRUE; +} + +static void +_update_buttons (GsUpdatesSection *self) +{ + /* operation in progress */ + if (self->cancellable != NULL) { + gtk_widget_set_sensitive (self->button_cancel, + !g_cancellable_is_cancelled (self->cancellable)); + gtk_stack_set_visible_child_name (self->button_stack, "cancel"); + gtk_widget_show (GTK_WIDGET (self->button_stack)); + return; + } + + if (self->kind == GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE || + self->kind == GS_UPDATES_SECTION_KIND_OFFLINE) { + if (_all_offline_updates_downloaded (self)) + gtk_stack_set_visible_child_name (self->button_stack, "update"); + else + gtk_stack_set_visible_child_name (self->button_stack, "download"); + + gtk_widget_show (GTK_WIDGET (self->button_stack)); + /* TRANSLATORS: This is the button for installing all + * offline updates */ + gtk_button_set_label (GTK_BUTTON (self->button_update), _("Restart & Update")); + } else if (self->kind == GS_UPDATES_SECTION_KIND_ONLINE) { + gtk_stack_set_visible_child_name (self->button_stack, "update"); + gtk_widget_show (GTK_WIDGET (self->button_stack)); + /* TRANSLATORS: This is the button for upgrading all + * online-updatable applications */ + gtk_button_set_label (GTK_BUTTON (self->button_update), _("Update All")); + } else { + gtk_widget_hide (GTK_WIDGET (self->button_stack)); + } + +} + +static void +_perform_update_cb (GsPluginLoader *plugin_loader, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsUpdatesSectionUpdateHelper) helper = (GsUpdatesSectionUpdateHelper *) user_data; + GsUpdatesSection *self = helper->self; + + /* get the results */ + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to perform update: %s", error->message); + goto out; + } + + /* trigger reboot if any application was not updatable live */ + if (helper->do_reboot) { + g_autoptr(GDBusConnection) bus = NULL; + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, + _reboot_failed_cb, + self); + + /* when we are not doing an offline update, show a notification + * if any application requires a reboot */ + } else if (helper->do_reboot_notification) { + gs_utils_reboot_notify (self->list); + } + +out: + g_clear_object (&self->cancellable); + _update_buttons (self); +} + +static void +_button_cancel_clicked_cb (GtkButton *button, GsUpdatesSection *self) +{ + g_cancellable_cancel (self->cancellable); + _update_buttons (self); +} + +static void +_download_finished_cb (GObject *object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GsUpdatesSection) self = (GsUpdatesSection *) user_data; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* get result */ + list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error); + if (list == NULL) { + if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("failed to download updates: %s", error->message); + } + + g_clear_object (&self->cancellable); + _update_buttons (self); +} + +static void +_button_download_clicked_cb (GtkButton *button, GsUpdatesSection *self) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + g_autoptr(GsPluginJob) plugin_job = NULL; + + g_set_object (&self->cancellable, cancellable); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD, + "list", self->list, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE, + "interactive", TRUE, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + (GAsyncReadyCallback) _download_finished_cb, + g_object_ref (self)); + _update_buttons (self); +} + +static void +_button_update_all_clicked_cb (GtkButton *button, GsUpdatesSection *self) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new (); + g_autoptr(GsPluginJob) plugin_job = NULL; + GsUpdatesSectionUpdateHelper *helper = g_new0 (GsUpdatesSectionUpdateHelper, 1); + + helper->self = g_object_ref (self); + + /* look at each app in turn */ + for (guint i = 0; i < gs_app_list_length (self->list); i++) { + GsApp *app = gs_app_list_index (self->list, i); + if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE) + helper->do_reboot = TRUE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) + helper->do_reboot_notification = TRUE; + } + + g_set_object (&self->cancellable, cancellable); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "list", self->list, + "interactive", TRUE, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + (GAsyncReadyCallback) _perform_update_cb, + helper); + _update_buttons (self); +} + +static GtkWidget * +_build_section_header (GsUpdatesSection *self) +{ + GtkStyleContext *context; + GtkWidget *header; + GtkWidget *label; + + /* get labels and buttons for everything */ + if (self->kind == GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE) { + /* TRANSLATORS: This is the header for system firmware that + * requires a reboot to apply */ + label = gtk_label_new (_("Integrated Firmware")); + } else if (self->kind == GS_UPDATES_SECTION_KIND_OFFLINE) { + /* TRANSLATORS: This is the header for offline OS and offline + * app updates that require a reboot to apply */ + label = gtk_label_new (_("Requires Restart")); + } else if (self->kind == GS_UPDATES_SECTION_KIND_ONLINE) { + /* TRANSLATORS: This is the header for online runtime and + * app updates, typically flatpaks or snaps */ + label = gtk_label_new (_("Application Updates")); + } else if (self->kind == GS_UPDATES_SECTION_KIND_ONLINE_FIRMWARE) { + /* TRANSLATORS: This is the header for device firmware that can + * be installed online */ + label = gtk_label_new (_("Device Firmware")); + } else { + g_assert_not_reached (); + } + + /* create header */ + header = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + context = gtk_widget_get_style_context (header); + gtk_style_context_add_class (context, "app-listbox-header"); + + /* put label into the header */ + gtk_widget_set_hexpand (label, TRUE); + gtk_container_add (GTK_CONTAINER (header), label); + gtk_widget_set_visible (label, TRUE); + gtk_widget_set_margin_start (label, 6); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + context = gtk_widget_get_style_context (label); + gtk_style_context_add_class (context, "app-listbox-header-title"); + + /* use a stack so we can switch which buttons are showing without the + * sizegroup resizing */ + self->button_stack = GTK_STACK (gtk_stack_new ()); + gtk_container_add (GTK_CONTAINER (header), GTK_WIDGET (self->button_stack)); + gtk_container_child_set (GTK_CONTAINER (header), GTK_WIDGET (self->button_stack), "pack-type", GTK_PACK_END, NULL); + + /* add download button */ + self->button_download = gs_progress_button_new (); + gtk_button_set_use_underline (GTK_BUTTON (self->button_download), TRUE); + gtk_button_set_label (GTK_BUTTON (self->button_download), _("_Download")); + context = gtk_widget_get_style_context (self->button_download); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUGGESTED_ACTION); + g_signal_connect (self->button_download, "clicked", + G_CALLBACK (_button_download_clicked_cb), + self); + gtk_stack_add_named (self->button_stack, self->button_download, "download"); + gtk_widget_set_visible (self->button_download, TRUE); + + /* add update button */ + self->button_update = gs_progress_button_new (); + context = gtk_widget_get_style_context (self->button_update); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUGGESTED_ACTION); + g_signal_connect (self->button_update, "clicked", + G_CALLBACK (_button_update_all_clicked_cb), + self); + gtk_stack_add_named (self->button_stack, self->button_update, "update"); + gtk_widget_set_visible (self->button_update, TRUE); + + /* add cancel button */ + self->button_cancel = gs_progress_button_new (); + gtk_button_set_label (GTK_BUTTON (self->button_cancel), _("Cancel")); + gs_progress_button_set_show_progress (GS_PROGRESS_BUTTON (self->button_cancel), TRUE); + g_signal_connect (self->button_cancel, "clicked", + G_CALLBACK (_button_cancel_clicked_cb), + self); + gtk_stack_add_named (self->button_stack, self->button_cancel, "cancel"); + gtk_widget_set_visible (self->button_cancel, TRUE); + + /* success */ + return header; +} + +static void +_list_header_func (GtkListBoxRow *row, GtkListBoxRow *before, gpointer user_data) +{ + GsUpdatesSection *self = GS_UPDATES_SECTION (user_data); + GtkWidget *header; + + /* section changed */ + if (before == NULL) { + if (gtk_list_box_row_get_header (row) != self->section_header) { + gtk_widget_unparent (self->section_header); + } + header = self->section_header; + } else { + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + } + gtk_list_box_row_set_header (row, header); +} + +static void +_app_row_activated_cb (GtkListBox *list_box, GtkListBoxRow *row, GsUpdatesSection *self) +{ + GsApp *app = gs_app_row_get_app (GS_APP_ROW (row)); + GtkWidget *dialog; + g_autofree gchar *str = NULL; + + /* debug */ + str = gs_app_to_string (app); + g_debug ("%s", str); + + dialog = gs_update_dialog_new (self->plugin_loader); + gs_update_dialog_show_update_details (GS_UPDATE_DIALOG (dialog), app); + gs_shell_modal_dialog_present (gs_page_get_shell (self->page), GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + +static void +gs_updates_section_show (GtkWidget *widget) +{ + _update_buttons (GS_UPDATES_SECTION (widget)); + + GTK_WIDGET_CLASS (gs_updates_section_parent_class)->show (widget); +} + +static void +gs_updates_section_dispose (GObject *object) +{ + GsUpdatesSection *self = GS_UPDATES_SECTION (object); + + g_clear_object (&self->cancellable); + g_clear_object (&self->list); + g_clear_object (&self->plugin_loader); + g_clear_object (&self->page); + g_clear_object (&self->sizegroup_image); + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_desc); + g_clear_object (&self->sizegroup_button); + g_clear_object (&self->sizegroup_header); + self->button_download = NULL; + self->button_update = NULL; + self->button_cancel = NULL; + self->button_stack = NULL; + g_clear_object (&self->section_header); + + G_OBJECT_CLASS (gs_updates_section_parent_class)->dispose (object); +} + +static void +gs_updates_section_class_init (GsUpdatesSectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_updates_section_dispose; + widget_class->show = gs_updates_section_show; +} + +void +gs_updates_section_set_size_groups (GsUpdatesSection *self, + GtkSizeGroup *image, + GtkSizeGroup *name, + GtkSizeGroup *desc, + GtkSizeGroup *button, + GtkSizeGroup *header) +{ + g_set_object (&self->sizegroup_image, image); + g_set_object (&self->sizegroup_name, name); + g_set_object (&self->sizegroup_desc, desc); + g_set_object (&self->sizegroup_button, button); + g_set_object (&self->sizegroup_header, header); + + gtk_size_group_add_widget (self->sizegroup_button, GTK_WIDGET (self->button_stack)); + gtk_size_group_add_widget (self->sizegroup_header, self->section_header); +} + +static void +gs_updates_section_progress_notify_cb (GsAppList *list, + GParamSpec *pspec, + GsUpdatesSection *self) +{ + if (self->button_cancel == NULL) + return; + + gs_progress_button_set_progress (GS_PROGRESS_BUTTON (self->button_cancel), + gs_app_list_get_progress (list)); +} + +static void +gs_updates_section_init (GsUpdatesSection *self) +{ + GtkStyleContext *context; + + self->list = gs_app_list_new (); + gs_app_list_add_flag (self->list, + GS_APP_LIST_FLAG_WATCH_APPS | + GS_APP_LIST_FLAG_WATCH_APPS_ADDONS | + GS_APP_LIST_FLAG_WATCH_APPS_RELATED); + g_signal_connect_object (self->list, "notify::progress", + G_CALLBACK (gs_updates_section_progress_notify_cb), + self, 0); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (self), + GTK_SELECTION_NONE); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self), + _list_sort_func, + self, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (self), + _list_header_func, + self, NULL); + g_signal_connect (self, "row-activated", + G_CALLBACK (_app_row_activated_cb), self); + gtk_widget_set_margin_top (GTK_WIDGET (self), 24); + + /* make rounded edges */ + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + gtk_style_context_add_class (context, "app-updates-section"); +} + +GtkListBox * +gs_updates_section_new (GsUpdatesSectionKind kind, + GsPluginLoader *plugin_loader, + GsPage *page) +{ + GsUpdatesSection *self; + self = g_object_new (GS_TYPE_UPDATES_SECTION, NULL); + self->kind = kind; + self->plugin_loader = g_object_ref (plugin_loader); + self->page = g_object_ref (page); + self->section_header = g_object_ref_sink (_build_section_header (self)); + return GTK_LIST_BOX (self); +} diff --git a/src/gs-updates-section.h b/src/gs-updates-section.h new file mode 100644 index 0000000..852a0b5 --- /dev/null +++ b/src/gs-updates-section.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gs-app-list.h" +#include "gs-plugin-loader.h" +#include "gs-page.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPDATES_SECTION (gs_updates_section_get_type ()) + +G_DECLARE_FINAL_TYPE (GsUpdatesSection, gs_updates_section, GS, UPDATES_SECTION, GtkListBox) + +typedef enum { + GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE, + GS_UPDATES_SECTION_KIND_OFFLINE, + GS_UPDATES_SECTION_KIND_ONLINE, + GS_UPDATES_SECTION_KIND_ONLINE_FIRMWARE, + GS_UPDATES_SECTION_KIND_LAST +} GsUpdatesSectionKind; + +GtkListBox *gs_updates_section_new (GsUpdatesSectionKind kind, + GsPluginLoader *plugin_loader, + GsPage *page); +GsAppList *gs_updates_section_get_list (GsUpdatesSection *self); +void gs_updates_section_add_app (GsUpdatesSection *self, + GsApp *app); +void gs_updates_section_remove_all (GsUpdatesSection *self); +void gs_updates_section_set_size_groups (GsUpdatesSection *self, + GtkSizeGroup *image, + GtkSizeGroup *name, + GtkSizeGroup *desc, + GtkSizeGroup *button, + GtkSizeGroup *header); + +G_END_DECLS diff --git a/src/gs-upgrade-banner.c b/src/gs-upgrade-banner.c new file mode 100644 index 0000000..b38a353 --- /dev/null +++ b/src/gs-upgrade-banner.c @@ -0,0 +1,411 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <stdlib.h> + +#include "gs-upgrade-banner.h" +#include "gs-common.h" + +typedef struct +{ + GsApp *app; + + GtkWidget *box_upgrades; + GtkWidget *button_upgrades_download; + GtkWidget *button_upgrades_install; + GtkWidget *button_upgrades_help; + GtkWidget *button_upgrades_cancel; + GtkWidget *label_upgrades_summary; + GtkWidget *label_upgrades_title; + GtkWidget *label_upgrades_warning; + GtkWidget *progressbar; + guint progress_pulse_id; + GtkCssProvider *banner_provider; /* (owned) (nullable) */ +} GsUpgradeBannerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsUpgradeBanner, gs_upgrade_banner, GTK_TYPE_BIN) + +enum { + SIGNAL_DOWNLOAD_CLICKED, + SIGNAL_INSTALL_CLICKED, + SIGNAL_HELP_CLICKED, + SIGNAL_CANCEL_CLICKED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +static gboolean +_pulse_cb (gpointer user_data) +{ + GsUpgradeBanner *self = GS_UPGRADE_BANNER (user_data); + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + gtk_progress_bar_pulse (GTK_PROGRESS_BAR (priv->progressbar)); + + return G_SOURCE_CONTINUE; +} + +static void +stop_progress_pulsing (GsUpgradeBanner *self) +{ + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + if (priv->progress_pulse_id != 0) { + g_source_remove (priv->progress_pulse_id); + priv->progress_pulse_id = 0; + } +} + +static void +gs_upgrade_banner_refresh (GsUpgradeBanner *self) +{ + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + const gchar *uri; + g_autofree gchar *name_bold = NULL; + g_autofree gchar *version_bold = NULL; + g_autofree gchar *str = NULL; + guint percentage; + + if (priv->app == NULL) + return; + + /* embolden */ + name_bold = g_strdup_printf ("<b>%s</b>", gs_app_get_name (priv->app)); + version_bold = g_strdup_printf ("<b>%s</b>", gs_app_get_version (priv->app)); + + /* Distributions that need to reboot to deploy the upgrade show the "Install" button */ + if (gs_app_has_quirk (priv->app, GS_APP_QUIRK_NEEDS_REBOOT)) { + gtk_button_set_label (GTK_BUTTON (priv->button_upgrades_install), + _("_Install")); + gtk_label_set_text (GTK_LABEL (priv->label_upgrades_warning), + _("It is recommended that you back up your " + "data and files before upgrading.")); + } else { + gtk_button_set_label (GTK_BUTTON (priv->button_upgrades_install), + _("_Restart Now")); + gtk_label_set_text (GTK_LABEL (priv->label_upgrades_warning), + _("Updates will be applied when the " + "computer is restarted.")); + } + + /* Refresh the title. Normally a distro upgrade state goes from + * + * AVAILABLE (available to download) to + * INSTALLING (downloading packages for later installation) to + * UPDATABLE (packages are downloaded and upgrade is ready to go) + */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_AVAILABLE: + /* TRANSLATORS: This is the text displayed when a distro + * upgrade is available. First %s is the distro name and the + * 2nd %s is the version, e.g. "Fedora 23 Now Available" */ + str = g_strdup_printf (_("%s %s Now Available"), + name_bold, version_bold); + gtk_label_set_markup (GTK_LABEL (priv->label_upgrades_title), str); + gtk_widget_set_visible (priv->label_upgrades_warning, FALSE); + gtk_widget_set_visible (priv->button_upgrades_cancel, FALSE); + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + /* TRANSLATORS: This is the text displayed while waiting to + * download a distro upgrade. First %s is the distro name and + * the 2nd %s is the version, e.g. "Waiting to Download Fedora 23" */ + str = g_strdup_printf (_("Waiting to Download %s %s"), + name_bold, version_bold); + gtk_label_set_markup (GTK_LABEL (priv->label_upgrades_title), str); + gtk_widget_set_visible (priv->label_upgrades_warning, FALSE); + gtk_widget_set_visible (priv->button_upgrades_cancel, TRUE); + break; + case AS_APP_STATE_INSTALLING: + /* TRANSLATORS: This is the text displayed while downloading a + * distro upgrade. First %s is the distro name and the 2nd %s + * is the version, e.g. "Downloading Fedora 23" */ + str = g_strdup_printf (_("Downloading %s %s"), + name_bold, version_bold); + gtk_label_set_markup (GTK_LABEL (priv->label_upgrades_title), str); + gtk_widget_set_visible (priv->label_upgrades_warning, FALSE); + gtk_widget_set_visible (priv->button_upgrades_cancel, TRUE); + break; + case AS_APP_STATE_UPDATABLE: + /* TRANSLATORS: This is the text displayed when a distro + * upgrade has been downloaded and is ready to be installed. + * First %s is the distro name and the 2nd %s is the version, + * e.g. "Fedora 23 Ready to be Installed" */ + str = g_strdup_printf (_("%s %s Ready to be Installed"), + name_bold, version_bold); + gtk_label_set_markup (GTK_LABEL (priv->label_upgrades_title), str); + gtk_widget_set_visible (priv->label_upgrades_warning, TRUE); + gtk_widget_set_visible (priv->button_upgrades_cancel, FALSE); + break; + default: + g_critical ("Unexpected app state ‘%s’ of app ‘%s’", + as_app_state_to_string (gs_app_get_state (priv->app)), + gs_app_get_unique_id (priv->app)); + break; + } + + /* Hide the upgrade box until the app state is known. */ + gtk_widget_set_visible (priv->box_upgrades, + (gs_app_get_state (priv->app) != AS_APP_STATE_UNKNOWN)); + + /* Refresh the summary if we got anything better than the default blurb */ + if (gs_app_get_summary (priv->app) != NULL) + gtk_label_set_text (GTK_LABEL (priv->label_upgrades_summary), + gs_app_get_summary (priv->app)); + + /* Show the right buttons for the current state */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_AVAILABLE: + gtk_widget_show (priv->button_upgrades_download); + gtk_widget_hide (priv->button_upgrades_install); + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + case AS_APP_STATE_INSTALLING: + gtk_widget_hide (priv->button_upgrades_download); + gtk_widget_hide (priv->button_upgrades_install); + break; + case AS_APP_STATE_UPDATABLE: + gtk_widget_hide (priv->button_upgrades_download); + gtk_widget_show (priv->button_upgrades_install); + break; + default: + g_critical ("Unexpected app state ‘%s’ of app ‘%s’", + as_app_state_to_string (gs_app_get_state (priv->app)), + gs_app_get_unique_id (priv->app)); + break; + } + + /* only show help when we have a URL */ + uri = gs_app_get_url (priv->app, AS_URL_KIND_HOMEPAGE); + gtk_widget_set_visible (priv->button_upgrades_help, uri != NULL); + + /* do a fill bar for the current progress */ + switch (gs_app_get_state (priv->app)) { + case AS_APP_STATE_INSTALLING: + percentage = gs_app_get_progress (priv->app); + if (percentage == GS_APP_PROGRESS_UNKNOWN) { + if (priv->progress_pulse_id == 0) + priv->progress_pulse_id = g_timeout_add (50, _pulse_cb, self); + + gtk_widget_set_visible (priv->progressbar, TRUE); + break; + } else if (percentage <= 100) { + stop_progress_pulsing (self); + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->progressbar), + (gdouble) percentage / 100.f); + gtk_widget_set_visible (priv->progressbar, TRUE); + break; + } + break; + default: + gtk_widget_hide (priv->progressbar); + stop_progress_pulsing (self); + break; + } +} + +static gboolean +app_refresh_idle (gpointer user_data) +{ + GsUpgradeBanner *self = GS_UPGRADE_BANNER (user_data); + + gs_upgrade_banner_refresh (self); + + g_object_unref (self); + return G_SOURCE_REMOVE; +} + +static void +app_state_changed (GsApp *app, GParamSpec *pspec, GsUpgradeBanner *self) +{ + g_idle_add (app_refresh_idle, g_object_ref (self)); +} + +static void +app_progress_changed (GsApp *app, GParamSpec *pspec, GsUpgradeBanner *self) +{ + g_idle_add (app_refresh_idle, g_object_ref (self)); +} + +static void +download_button_cb (GtkWidget *widget, GsUpgradeBanner *self) +{ + g_signal_emit (self, signals[SIGNAL_DOWNLOAD_CLICKED], 0); +} + +static void +install_button_cb (GtkWidget *widget, GsUpgradeBanner *self) +{ + g_signal_emit (self, signals[SIGNAL_INSTALL_CLICKED], 0); +} + +static void +learn_more_button_cb (GtkWidget *widget, GsUpgradeBanner *self) +{ + g_signal_emit (self, signals[SIGNAL_HELP_CLICKED], 0); +} + +static void +cancel_button_cb (GtkWidget *widget, GsUpgradeBanner *self) +{ + g_signal_emit (self, signals[SIGNAL_CANCEL_CLICKED], 0); +} + +void +gs_upgrade_banner_set_app (GsUpgradeBanner *self, GsApp *app) +{ + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + const gchar *css; + + g_return_if_fail (GS_IS_UPGRADE_BANNER (self)); + g_return_if_fail (GS_IS_APP (app) || app == NULL); + + if (priv->app) { + g_signal_handlers_disconnect_by_func (priv->app, app_state_changed, self); + g_signal_handlers_disconnect_by_func (priv->app, app_progress_changed, self); + } + + g_set_object (&priv->app, app); + if (!app) + return; + + g_signal_connect (priv->app, "notify::state", + G_CALLBACK (app_state_changed), self); + g_signal_connect (priv->app, "notify::progress", + G_CALLBACK (app_progress_changed), self); + + /* perhaps set custom css */ + css = gs_app_get_metadata_item (app, "GnomeSoftware::UpgradeBanner-css"); + gs_utils_widget_set_css (priv->box_upgrades, &priv->banner_provider, "upgrade-banner-custom", css); + + gs_upgrade_banner_refresh (self); +} + +GsApp * +gs_upgrade_banner_get_app (GsUpgradeBanner *self) +{ + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + g_return_val_if_fail (GS_IS_UPGRADE_BANNER (self), NULL); + + return priv->app; +} + +static void +gs_upgrade_banner_dispose (GObject *object) +{ + GsUpgradeBanner *self = GS_UPGRADE_BANNER (object); + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + g_clear_object (&priv->banner_provider); + + G_OBJECT_CLASS (gs_upgrade_banner_parent_class)->dispose (object); +} + +static void +gs_upgrade_banner_destroy (GtkWidget *widget) +{ + GsUpgradeBanner *self = GS_UPGRADE_BANNER (widget); + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + stop_progress_pulsing (self); + + if (priv->app) { + g_signal_handlers_disconnect_by_func (priv->app, app_state_changed, self); + g_signal_handlers_disconnect_by_func (priv->app, app_progress_changed, self); + } + + g_clear_object (&priv->app); + + GTK_WIDGET_CLASS (gs_upgrade_banner_parent_class)->destroy (widget); +} + +static void +gs_upgrade_banner_init (GsUpgradeBanner *self) +{ + GsUpgradeBannerPrivate *priv = gs_upgrade_banner_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (priv->button_upgrades_download, "clicked", + G_CALLBACK (download_button_cb), + self); + g_signal_connect (priv->button_upgrades_install, "clicked", + G_CALLBACK (install_button_cb), + self); + g_signal_connect (priv->button_upgrades_help, "clicked", + G_CALLBACK (learn_more_button_cb), + self); + g_signal_connect (priv->button_upgrades_cancel, "clicked", + G_CALLBACK (cancel_button_cb), + self); +} + +static void +gs_upgrade_banner_class_init (GsUpgradeBannerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_upgrade_banner_dispose; + widget_class->destroy = gs_upgrade_banner_destroy; + + signals [SIGNAL_DOWNLOAD_CLICKED] = + g_signal_new ("download-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsUpgradeBannerClass, download_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_INSTALL_CLICKED] = + g_signal_new ("install-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsUpgradeBannerClass, install_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_CANCEL_CLICKED] = + g_signal_new ("cancel-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsUpgradeBannerClass, cancel_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_HELP_CLICKED] = + g_signal_new ("help-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsUpgradeBannerClass, help_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-upgrade-banner.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, box_upgrades); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, button_upgrades_download); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, button_upgrades_install); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, button_upgrades_cancel); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, button_upgrades_help); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, label_upgrades_summary); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, label_upgrades_title); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, progressbar); + gtk_widget_class_bind_template_child_private (widget_class, GsUpgradeBanner, label_upgrades_warning); +} + +GtkWidget * +gs_upgrade_banner_new (void) +{ + GsUpgradeBanner *self; + self = g_object_new (GS_TYPE_UPGRADE_BANNER, + "vexpand", FALSE, + NULL); + return GTK_WIDGET (self); +} diff --git a/src/gs-upgrade-banner.h b/src/gs-upgrade-banner.h new file mode 100644 index 0000000..c8c2bf8 --- /dev/null +++ b/src/gs-upgrade-banner.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_UPGRADE_BANNER (gs_upgrade_banner_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsUpgradeBanner, gs_upgrade_banner, GS, UPGRADE_BANNER, GtkBin) + +struct _GsUpgradeBannerClass +{ + GtkBinClass parent_class; + + void (*download_clicked) (GsUpgradeBanner *self); + void (*install_clicked) (GsUpgradeBanner *self); + void (*cancel_clicked) (GsUpgradeBanner *self); + void (*help_clicked) (GsUpgradeBanner *self); +}; + +GtkWidget *gs_upgrade_banner_new (void); +void gs_upgrade_banner_set_app (GsUpgradeBanner *self, + GsApp *app); +GsApp *gs_upgrade_banner_get_app (GsUpgradeBanner *self); + +G_END_DECLS diff --git a/src/gs-upgrade-banner.ui b/src/gs-upgrade-banner.ui new file mode 100644 index 0000000..3e53a25 --- /dev/null +++ b/src/gs-upgrade-banner.ui @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="GsUpgradeBanner" parent="GtkBin"> + <child> + <object class="GtkBox" id="box_upgrades"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="valign">center</property> + <style> + <class name="upgrade-banner"/> + </style> + <child> + <object class="GtkLabel" id="label_upgrades_title"> + <property name="visible">True</property> + <property name="margin-top">66</property> + <property name="margin_bottom">26</property> + <!-- Just a placeholder; actual label text is set in code --> + <property name="label">GNOME 3.20 Now Available</property> + <attributes> + <attribute name="scale" value="1.8"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label_upgrades_summary"> + <property name="visible">True</property> + <property name="label" translatable="yes">A major upgrade, with new features and added polish.</property> + <attributes> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkBox" id="box_upgrades_buttons"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <property name="halign">fill</property> + <property name="valign">end</property> + <property name="spacing">12</property> + <property name="margin_top">48</property> + <style> + <class name="osd"/> + <class name="upgrade-buttons"/> + </style> + <child> + <object class="GtkButton" id="button_upgrades_help"> + <property name="label" translatable="yes">_Learn More</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="relief">normal</property> + </object> + </child> + + <child> + <object class="GtkLabel" id="label_dummy1"> + <property name="visible">True</property> + <property name="label"></property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + </object> + </child> + + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="width_request">400</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="fraction">0.3</property> + <property name="margin_top">8</property> + <style> + <class name="upgrade-progressbar"/> + </style> + </object> + </child> + + <child> + <object class="GtkLabel" id="label_upgrades_warning"> + <property name="visible">True</property> + <property name="label"></property><!-- Set in code --> + <property name="justify">center</property> + <property name="wrap">True</property> + </object> + </child> + + <child> + <object class="GtkLabel" id="label_dummy2"> + <property name="visible">True</property> + <property name="label"></property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + </object> + </child> + + <child> + <object class="GtkButton" id="button_upgrades_download"> + <property name="label" translatable="yes">_Download</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="relief">normal</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_upgrades_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="relief">normal</property> + </object> + </child> + <child> + <object class="GtkButton" id="button_upgrades_install"> + <property name="label">_Install</property><!-- Set in code --> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="relief">normal</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gs-vendor.c b/src/gs-vendor.c new file mode 100644 index 0000000..c87d7cd --- /dev/null +++ b/src/gs-vendor.c @@ -0,0 +1,126 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-vendor.h" + +struct _GsVendor +{ + GObject parent_instance; + + GKeyFile *file; +}; + +G_DEFINE_TYPE (GsVendor, gs_vendor, G_TYPE_OBJECT) + +#ifdef HAVE_PACKAGEKIT +static const gchar * +gs_vendor_type_to_string (GsVendorUrlType type) +{ + if (type == GS_VENDOR_URL_TYPE_CODEC) + return "CodecUrl"; + if (type == GS_VENDOR_URL_TYPE_FONT) + return "FontUrl"; + if (type == GS_VENDOR_URL_TYPE_MIME) + return "MimeUrl"; + if (type == GS_VENDOR_URL_TYPE_HARDWARE) + return "HardwareUrl"; + return "DefaultUrl"; +} +#endif + +gchar * +gs_vendor_get_not_found_url (GsVendor *vendor, GsVendorUrlType type) +{ +#ifdef HAVE_PACKAGEKIT + const gchar *key; + gchar *url = NULL; + + /* get data */ + key = gs_vendor_type_to_string (type); + url = g_key_file_get_string (vendor->file, "PackagesNotFound", key, NULL); + + /* none is a special value */ + if (g_strcmp0 (url, "none") == 0) { + g_free (url); + url = NULL; + } + + /* got a valid URL */ + if (url != NULL) + goto out; + + /* default has no fallback */ + if (type == GS_VENDOR_URL_TYPE_DEFAULT) + goto out; + + /* get fallback data */ + g_debug ("using fallback"); + key = gs_vendor_type_to_string (GS_VENDOR_URL_TYPE_DEFAULT); + url = g_key_file_get_string (vendor->file, "PackagesNotFound", key, NULL); + + /* none is a special value */ + if (g_strcmp0 (url, "none") == 0) { + g_free (url); + url = NULL; + } +out: + g_debug ("url=%s", url); + return url; +#else + return NULL; +#endif +} + +static void +gs_vendor_init (GsVendor *vendor) +{ +#ifdef HAVE_PACKAGEKIT + const gchar *fn = "/etc/PackageKit/Vendor.conf"; + gboolean ret; + + vendor->file = g_key_file_new (); + ret = g_key_file_load_from_file (vendor->file, fn, G_KEY_FILE_NONE, NULL); + if (!ret) + g_warning ("%s file not found", fn); +#endif +} + +static void +gs_vendor_finalize (GObject *object) +{ + GsVendor *vendor = GS_VENDOR (object); + + if (vendor->file != NULL) + g_key_file_free (vendor->file); + + G_OBJECT_CLASS (gs_vendor_parent_class)->finalize (object); +} + +static void +gs_vendor_class_init (GsVendorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_vendor_finalize; +} + +/** + * gs_vendor_new: + * + * Return value: a new GsVendor object. + **/ +GsVendor * +gs_vendor_new (void) +{ + GsVendor *vendor; + vendor = g_object_new (GS_TYPE_VENDOR, NULL); + return GS_VENDOR (vendor); +} + diff --git a/src/gs-vendor.h b/src/gs-vendor.h new file mode 100644 index 0000000..68c2521 --- /dev/null +++ b/src/gs-vendor.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_VENDOR (gs_vendor_get_type ()) + +G_DECLARE_FINAL_TYPE (GsVendor, gs_vendor, GS, VENDOR, GObject) + +typedef enum +{ + GS_VENDOR_URL_TYPE_CODEC, + GS_VENDOR_URL_TYPE_FONT, + GS_VENDOR_URL_TYPE_MIME, + GS_VENDOR_URL_TYPE_HARDWARE, + GS_VENDOR_URL_TYPE_DEFAULT +} GsVendorUrlType; + +GsVendor *gs_vendor_new (void); +gchar *gs_vendor_get_not_found_url (GsVendor *vendor, + GsVendorUrlType type); + +G_END_DECLS diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css new file mode 100644 index 0000000..1006649 --- /dev/null +++ b/src/gtk-style-hc.css @@ -0,0 +1,247 @@ +.installed-overlay-box { + font-size: smaller; + background-color: @theme_selected_bg_color; + border-radius: 4px; + color: @theme_selected_fg_color; +} + +.popular-installed-overlay-box { + font-size: smaller; + background-color: @theme_selected_bg_color; + border-radius: 0; + color: @theme_selected_fg_color; + text-shadow: none; +} + +.installed-icon, .app-row-installed-label { + color: #999999; +} + +.index-title-alignment-software { + font-weight: bold; + font-size: 125%; +} + +.needs-attention { + background-image: none; + background-color: shade(@theme_selected_bg_color, 1.1); + border-radius: 1px; +} + +.screenshot-image, .screenshot-image-thumb { + background-image: none; + background-color: shade(@theme_bg_color, 0.9); +} +.screenshot-image { border-radius: 5px; } +.screenshot-image-thumb { border-radius: 3px; } + +.app-tile-label { + font-size: 105%; +} + +.app-row-tag { + text-shadow: none; + color: white; + background-color: #999999; + font-size: smaller; + border-radius: 4px; + padding: 2px 10px; +} + +.view.tile { + padding: 0; + background-image: none; + background-color: mix(@theme_base_color,@theme_bg_color,0.3); +} + +.view.tile:hover { + background-color: @theme_base_color; +} + +.view.tile:active { + border-color: @theme_selected_bg_color; + box-shadow: none; + color: @theme_selected_bg_color; +} + +.view.tile:backdrop { + box-shadow: none; + border-color: @unfocused_borders; +} + +/* The rest of the featured-tile CSS is loaded at runtime in gs-feature-tile.c */ +.featured-tile { + all: unset; + padding: 0; + border-radius: 3px; + border-width: 1px; + border-image: none; +} + +.application-details-infobar.info { + background-color: #d3d7cf; + color: @theme_fg_color; + border-color: darker(#d3d7cf); + border-style: solid; + border-width: 1px; + text-shadow: none; +} + +.application-details-infobar.warning { + background-color: #fcaf3e; + color: @theme_fg_color; + border-color: darker(#fcaf3e); + border-style: solid; + border-width: 1px; + text-shadow: none; +} + +.application-details-title { + font-weight: bold; + font-size: 125%; +} + +.application-details-summary { +} + +.review-summary { + font-weight: bold; +} + +.application-details-description { +} + +.install-progress { + background-image: linear-gradient(to top, @theme_selected_bg_color 2px, alpha(@theme_selected_bg_color, 0) 2px); + background-repeat: no-repeat; + background-position: 0 bottom; + background-size: 0; + transition: none; +} + +.install-progress:dir(rtl) { background-position: 100% bottom; } + +.error-label { + text-shadow: none; +} + +.app-row-origin-text { + font-size: smaller; +} + +.app-listbox-header { + padding: 6px; + background-image: none; + background-color: #babdb6; + border-color: #000000; +} + +.app-updates-section { + border-radius: 4px; + border: 1px solid #000000; +} + +.app-listbox-header-title { + font-size: larger; +} + +.image-list { + background-color: transparent; +} + +box.star, GtkBox.star { + background-color: transparent; + background-image: none; +} + +button.star, .button.star { + outline-offset: 0; + background-color: transparent; + background-image: none; + border-image: none; + border-radius: 0; + border-width: 0px; + padding: 0; + box-shadow: none; + outline-offset: -1px; +} + +/* for the review dialog */ +.star-enabled { + color: #000000; +} +.star-disabled { + color: #777777; +} + +/* for the app details shell */ +.star-enabled:disabled { + color: #000000; +} +.star-disabled:disabled { + color: #777777; +} + +.counter-label { + text-shadow: none; + color: @theme_selected_fg_color; + background-color: mix(@theme_selected_bg_color, @theme_selected_fg_color, 0.3); + font-size: smaller; + border-radius: 4px; + padding: 0px 4px; +} + +/* the following two selectors are to color the small gap before the list inside the scrolled window + setting a background on the scrolled window affects the undershoot and the overshoot so explicitelly + excluding with :not() */ +.category-sidebar:not(.undershoot):not(.overshoot) { background-color: @theme_base_color; } + +.category-sidebar:backdrop:not(.undershoot):not(.overshoot) { background-color: @theme_unfocused_base_color; } + +/* Superfluous borders removal */ +.category-sidebar { + border-style: none; +} + +.category-sidebar:dir(rtl) { + border-left-style: solid; +} + +.category-sidebar:dir(ltr) { + border-right-style: solid; +} + +.dimmer-label { + opacity: 0.25; + text-shadow: none; +} + +.update-failed-details { + font-family: Monospace; + font-size: 90%; + padding: 16px; +} + +.upgrade-banner { + background-color: #1c5288; + padding: 0px; + border-radius: 4px; + border: 1px solid darker(@theme_bg_color); + color: @theme_selected_fg_color; +} + +.upgrade-buttons { + padding: 18px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.upgrade-progressbar { + box-shadow: none +} + +.eol-box { + background-color: @theme_selected_bg_color; + border: 1px solid shade(@theme_selected_bg_color, 0.8); + color: @theme_selected_fg_color; +} diff --git a/src/gtk-style.css b/src/gtk-style.css new file mode 100644 index 0000000..4b5fcef --- /dev/null +++ b/src/gtk-style.css @@ -0,0 +1,523 @@ +.installed-overlay-box { + font-size: smaller; + background-color: @theme_selected_bg_color; + border-radius: 0; + color: @theme_selected_fg_color; + text-shadow: 0 1px 0 rgba(0,0,0,0.5); +} + .installed-overlay-box:backdrop label { + color: @theme_selected_fg_color; + } + +.installed-icon { + color: @theme_selected_bg_color; +} + +.popular-installed-overlay-box { + font-size: smaller; + background-color: @theme_selected_bg_color; + border-radius: 0; + color: @theme_selected_fg_color; + text-shadow: none; +} + .popular-installed-overlay-box:backdrop label { + color: @theme_selected_fg_color; + } + +.index-title-alignment-software { + font-weight: bold; + font-size: 125%; +} + +.app-row-installed-label { + color: @theme_selected_bg_color; + font-size: smaller; +} + +.app-row-app-size { + font-size: x-small; +} + +.needs-attention { + background-image: none; + background-color: shade(@theme_selected_bg_color, 1.1); + border-radius: 1px; +} + +.toolbar-primary-buttons-software { + padding-left: 26px; + padding-right: 26px; +} + +.round-button { + border-radius: 16px; + -gtk-outline-radius: 16px; +} + +.details-license-free, +.details-license-nonfree, +.details-license-unknown, +.details-license-free:backdrop, +.details-license-nonfree:backdrop, +.details-license-unknown:backdrop { + outline-offset: 0; + background-image: none; + border-image: none; + border-radius: 4px; + border-width: 0 0 2px 0; + padding: 1px 9px; + box-shadow: none; + text-shadow: none; + color: #ffffff; +} + +.details-license-free label, +.details-license-free:backdrop label, +.details-license-free:hover label, +.details-license-nonfree label, +.details-license-nonfree:backdrop label, +.details-license-nonfree:hover label { + color: #fff; +} + +.details-license-unknown label, +.details-license-unknown:backdrop label, +.details-license-unknown:hover label { + color: #373d3f; +} + +.content-rating { + outline-offset: 0; + background-image: none; + background-color: #dbdbdb; + border-image: none; + border-radius: 4px; + border-width: 0px; + padding: 1px 9px; + box-shadow: none; + text-shadow: none; +} + +.details-license-free { + background-color: #4e9a06; + border-color: #3e7905; +} +.details-license-free:hover { + background-color: #5db807; + border-color: #4d9606; +} +.details-license-free:backdrop { + border-color: #4e9a06; +} + +.details-license-nonfree { + background-color: #ee2222; + border-color: #c20f0f; +} +.details-license-nonfree:hover { + background-color: #f25959; + border-color: #ed1b1b; +} +.details-license-nonfree:backdrop { + border-color: #ee2222; +} + +.details-license-unknown { + background-color: #dbdbdb; + border-color: #bbbbbb; +} +.details-license-unknown:hover { + background-color: #eeeeee; + border-color: #d5d5d5; +} +.details-license-unknown:backdrop { + border-color: #dbdbdb; +} + +.kudo-pill { + color: @theme_selected_fg_color; + background-color: shade(@theme_selected_bg_color, 1.1); + background-image: none; + border-radius: 16px; + padding: 8px; +} + +/* should be :disabled but we need to support older versions of GTK */ +.kudo-pill:disabled { + color: @theme_bg_color; + background-color: mix(@insensitive_fg_color, @theme_bg_color, 0.6); +} + +.kudo-pill:disabled:backdrop { + color: @theme_unfocused_bg_color; + background-color: mix(@insensitive_fg_color, @theme_unfocused_bg_color, 0.8); +} + +.onlyjustvisible:disabled { + opacity: 0.25; +} + +.screenshot-image, .screenshot-image-thumb { + background-image: none; + background-color: shade(@theme_bg_color, 0.9); +} +.screenshot-image { border-radius: 5px; } +.screenshot-image-thumb { border-radius: 3px; } + +.app-tile-label { + font-size: 105%; +} + +.app-row-tag { + text-shadow: none; + color: @theme_selected_fg_color; + background-color: #999999; + font-size: smaller; + border-radius: 4px; + padding: 2px 10px; +} + +.review-textbox { + padding: 6px; +} + +@define-color gs_tile_bg_color mix(@theme_base_color,@theme_bg_color,0.3); +@define-color gs_tile_borders mix(@gs_tile_bg_color,@theme_fg_color,0.3); +@define-color gs_tile_borders_alpha alpha(@theme_fg_color,0.3); + +.view.tile { + padding: 1px; + border: none; + box-shadow: inset 0 2px 0 @theme_base_color, + inset 0 -2px 0 mix(@gs_tile_bg_color,@gs_tile_borders,0.5), + inset 0 0 0 1px @gs_tile_borders, + inset 0 -3px 0 -2px shade(@gs_tile_borders,0.75); + background: @gs_tile_bg_color; +} + +.view.category-tile { + padding-top: 2px; + padding-bottom: 2px; +} + +.app-list { + background-color: @theme_base_color; +} + +.view.tile:hover { + background-color: @theme_base_color; +} + +/* Making some shadows transparent instead of replacing multiple shadows with + one shadow prevents some horrendous transition animations, which happen even + with backdrop transition disabled. */ + +.view.tile:active, +.view.tile.colorful:active { + background: @gs_tile_bg_color; + box-shadow: inset 0 2px 0 transparent, + inset 0 -2px 0 transparent, + inset 0 0 0 1px @theme_selected_bg_color, + inset 0 -3px 0 -2px transparent; + color: @theme_selected_bg_color; +} + +.view.tile:backdrop { + box-shadow: inset 0 2px 0 transparent, + inset 0 -2px 0 transparent, + inset 0 0 0 1px @unfocused_borders, + inset 0 -3px 0 -2px transparent; + /* Tile transitions are choppy for me for some reason. */ + transition: none; +} + +/* The rest of the featured-tile CSS is loaded at runtime in gs-feature-tile.c */ +.featured-tile { + all: unset; + padding: 0; + border-radius: 5px; /* match button tiles */ + border-width: 1px; + border-image: none; + box-shadow: none; + /* box-shadow: inset 0 0 0 1px alpha(@theme_fg_color,0.3), 0 0 1px alpha(black,0.4); */ +} + .featured-tile:backdrop label { + color: inherit; + text-shadow: none; + } + +.application-details-infobar.info { + background-color: shade(@theme_bg_color, 0.9); + color: @theme_fg_color; + border-color: darker(shade(@theme_bg_color, 0.9)); + border-style: solid; + border-width: 1px; + text-shadow: none; +} + +.application-details-infobar { + background-color: shade(@theme_bg_color, 0.9); + color: @theme_fg_color; + border-color: darker(shade(@theme_bg_color, 0.9)); + border-style: solid; + border-width: 1px; + padding: 12px; + text-shadow: none; +} + +.application-details-infobar.warning { + background-color: #fcaf3e; + color: #2e3436; + border-color: darker(#fcaf3e); + border-style: solid; + border-width: 1px; + text-shadow: none; +} + +.application-details-title { + font-weight: bold; + font-size: 125%; +} + +.application-details-webapp-warning { + font-weight: bold; +} + +.application-details-summary { +} + +.application-details-description { +} + +.install-progress { + background-image: linear-gradient(to top, @theme_selected_bg_color 2px, alpha(@theme_selected_bg_color, 0) 2px); + background-repeat: no-repeat; + background-position: 0 bottom; + background-size: 0; + transition: none; +} + +.install-progress:dir(rtl) { background-position: 100% bottom; } + +.review-summary { + font-weight: bold; +} + +.review-listbox { + all: unset; +} + +.review-row button, .review-row .button { font-size: smaller; } + +/* gtk+ 3.20+ only */ +.review-row .vote-buttons button { + margin-right: -1px; + + /* restricting transition properties since the hack for the separator*/ + transition-property: background, box-shadow, border-style, text-shadow; +} + +/* this is the separator between yes and no vote buttons, gtk+ 3.20 only */ +.review-row .vote-buttons button:not(:first-child):not(:hover):not(:active):not(:backdrop) { + border-image: linear-gradient(to top, @borders, @borders) 0 0 0 1 / 5px 0 5px 1px; +} +.review-row .vote-buttons button:not(:first-child):backdrop { + border-image: linear-gradient(to top, @unfocused_borders, @unfocused_borders) 0 0 0 1 / 5px 0 5px 1px; +} + +.reviewbar { + background-image: none; + background-color: #babdb6; + color: #555753; +} + +.error-label { + text-shadow: none; +} + +.version-arrow-label { + font-size: x-small; +} + +.overview-more-button { + font-size: smaller; + padding: 0px 15px; +} + +.app-row-origin-text { + font-size: smaller; +} + +.app-listbox-header { + padding: 6px; + background-image: none; + border-bottom: 1px solid @theme_bg_color; +} + +.app-listbox-header:dir(ltr) { padding-left: 10px; } + +.app-listbox-header:dir(rtl) { padding-right: 10px; } + +.app-updates-section { + border-radius: 4px; + border: 1px solid darker(@theme_bg_color); +} + +.app-listbox-header-title { + font-size: 100%; + font-weight: bold; +} + +.image-list { + background-color: transparent; +} + +box.star, GtkBox.star { + background-color: transparent; + background-image: none; +} + +button.star, .button.star { + outline-offset: 0; + background-color: transparent; + background-image: none; + border-image: none; + border-radius: 0; + border-width: 0px; + padding: 0; + box-shadow: none; + outline-offset: -1px; +} + +/* i have no idea why GTK adds padding here */ +flowboxchild { + padding: 0px; +} + +/* for the review dialog */ +.star-enabled, +.star-enabled:disabled { + color: shade(@theme_fg_color, 0.8); +} +.star-disabled, +.star-disabled:disabled { + color: shade(@theme_bg_color, 0.8); +} + +.counter-label { + text-shadow: none; + color: @theme_selected_fg_color; + background-color: mix(@theme_selected_bg_color, @theme_selected_fg_color, 0.3); + font-size: smaller; + border-radius: 4px; + padding: 0px 4px; +} + +/* the following two selectors are to color the small gap before the list inside the scrolled window + setting a background on the scrolled window affects the undershoot and the overshoot so explicitelly + excluding with :not() */ +.category-sidebar:not(.undershoot):not(.overshoot) { background-color: @theme_base_color; } + +.category-sidebar:backdrop:not(.undershoot):not(.overshoot) { background-color: @theme_unfocused_base_color; } + +/* padding removal */ +.list-box-app-row { + padding: 0px; +} + +/* Superfluous borders removal */ +.category-sidebar { + border-style: none; +} + +.category-sidebar:dir(rtl) { + border-left-style: solid; +} + +.category-sidebar:dir(ltr) { + border-right-style: solid; +} + +.dimmer-label { + opacity: 0.25; + text-shadow: none; +} + +.update-failed-details { + font-family: Monospace; + font-size: smaller; + padding: 16px; +} + +.upgrade-banner { + background-color: #1c5288; + padding: 0px; + border-radius: 4px; + border: 1px solid darker(@theme_bg_color); + color: @theme_selected_fg_color; +} + +.upgrade-buttons { + padding: 18px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.upgrade-progressbar { + box-shadow: none +} + +.eol-box { + background-color: @theme_selected_bg_color; + border: 1px solid shade(@theme_selected_bg_color, 0.8); + color: @theme_selected_fg_color; +} + +.category_page_header_filter_box .radio, .category_page_header_filter_box .radio:hover { + background-color: transparent; background-image: none; + box-shadow: none; + border: none; + border-radius: 0; + border-bottom: 4px solid transparent; +} + +.category_page_header_filter_box .radio:hover { + border-bottom-color: @theme_selected_bg_color; +} + +.category_page_header_filter_box .radio:checked { + border-bottom-color: @theme_selected_bg_color; +} + +/* uses theme_bg_color and a shade with the ratio of the original color */ +.category_page_header_filter_box { + background-color: shade(@theme_bg_color, 0.9); + border-bottom: 1px solid darker(shade(@theme_bg_color, 0.9)); +} + +.switcher-label { + opacity: 0.5; +} + +.featured-button-left, +.featured-button-right { + padding: 2px 5px; + margin: 6px; +} + +.featured-button-left:not(:hover), +.featured-button-right:not(:hover) { + background: transparent; + border:transparent; + box-shadow: none; +} + +/* these typographical classes will be provided in gtk eventually */ +.title-1{ + font-weight: 800; + font-size: 20pt; +} +.caption{ + font-weight: 400; + font-size: 10pt; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..6581e77 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,257 @@ +cargs = ['-DG_LOG_DOMAIN="Gs"'] +cargs += ['-DLOCALPLUGINDIR=""'] + +resources_src = gnome.compile_resources( + 'gs-resources', + 'gnome-software.gresource.xml', + source_dir : '.', + c_name : 'gs' +) + +gdbus_src = gnome.gdbus_codegen( + 'gs-shell-search-provider-generated', + 'shell-search-provider-dbus-interfaces.xml', + interface_prefix : 'org.gnome.', + namespace : 'Gs' +) + +gnome_software_sources = [ + 'gs-app-addon-row.c', + 'gs-application.c', + 'gs-app-row.c', + 'gs-app-tile.c', + 'gs-basic-auth-dialog.c', + 'gs-category-page.c', + 'gs-category-tile.c', + 'gs-common.c', + 'gs-css.c', + 'gs-content-rating.c', + 'gs-details-page.c', + 'gs-extras-page.c', + 'gs-feature-tile.c', + 'gs-first-run-dialog.c', + 'gs-fixed-size-bin.c', + 'gs-folders.c', + 'gs-hiding-box.c', + 'gs-history-dialog.c', + 'gs-info-bar.c', + 'gs-installed-page.c', + 'gs-language.c', + 'gs-loading-page.c', + 'gs-main.c', + 'gs-metered-data-dialog.c', + 'gs-moderate-page.c', + 'gs-overview-page.c', + 'gs-origin-popover-row.c', + 'gs-page.c', + 'gs-popular-tile.c', + 'gs-prefs-dialog.c', + 'gs-progress-button.c', + 'gs-removal-dialog.c', + 'gs-repos-dialog.c', + 'gs-repo-row.c', + 'gs-review-bar.c', + 'gs-review-dialog.c', + 'gs-review-histogram.c', + 'gs-review-row.c', + 'gs-screenshot-image.c', + 'gs-search-page.c', + 'gs-shell.c', + 'gs-shell-search-provider.c', + 'gs-star-widget.c', + 'gs-summary-tile.c', + 'gs-third-party-repo-row.c', + 'gs-update-dialog.c', + 'gs-update-list.c', + 'gs-update-monitor.c', + 'gs-updates-page.c', + 'gs-updates-section.c', + 'gs-upgrade-banner.c', + 'gs-vendor.c' +] + +gnome_software_dependencies = [ + appstream_glib, + gio_unix, + glib, + gmodule, + goa, + gtk, + json_glib, + libm, + libsoup, + libxmlb, +] + +if get_option('packagekit') + gnome_software_sources += [ + 'gs-dbus-helper.c', + ] + gnome_software_sources += gnome.gdbus_codegen( + 'gs-packagekit-generated', + 'org.freedesktop.PackageKit.xml', + interface_prefix : 'org.freedesktop.', + namespace : 'Gs' + ) + gnome_software_sources += gnome.gdbus_codegen( + 'gs-packagekit-modify2-generated', + 'org.freedesktop.PackageKit.Modify2.xml', + interface_prefix : 'org.freedesktop.', + namespace : 'Gs' + ) + gnome_software_dependencies += [packagekit] +endif + +if get_option('gnome_desktop') + gnome_software_dependencies += [gnome_desktop] +endif + +if get_option('gspell') + gnome_software_dependencies += [gspell] +endif + +if get_option('mogwai') + gnome_software_dependencies += [mogwai_schedule_client] +endif + +executable( + 'gnome-software', + resources_src, + gdbus_src, + sources : gnome_software_sources, + include_directories : [ + include_directories('..'), + include_directories('../lib'), + ], + dependencies : gnome_software_dependencies, + link_with : [ + libgnomesoftware + ], + c_args : cargs, + install : true, + install_dir : get_option('bindir') +) + +executable( + 'gnome-software-restarter', + sources : 'gs-restarter.c', + include_directories : [ + include_directories('..'), + ], + dependencies : [ + gio_unix, + glib, + ], + c_args : cargs, + install : true, + install_dir : get_option('libexecdir') +) + +# no quoting +cdata = configuration_data() +cdata.set('bindir', join_paths(get_option('prefix'), + get_option('bindir'))) + +# replace @bindir@ +configure_file( + input : 'org.gnome.Software.service.in', + output : 'org.gnome.Software.service', + install_dir: join_paths(get_option('datadir'), 'dbus-1/services'), + configuration : cdata +) + +# replace @bindir@ +configure_file( + input : 'gnome-software-service.desktop.in', + output : 'gnome-software-service.desktop', + install_dir: join_paths(get_option('sysconfdir'), 'xdg/autostart'), + configuration : cdata +) + +# replace @bindir@ +i18n.merge_file( + input: 'org.gnome.Software.desktop.in', + output: 'org.gnome.Software.desktop', + type: 'desktop', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +i18n.merge_file( + input: 'gnome-software-local-file.desktop.in', + output: 'gnome-software-local-file.desktop', + type: 'desktop', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +install_data('org.gnome.Software-search-provider.ini', + install_dir : 'share/gnome-shell/search-providers') + +if get_option('man') + xsltproc = find_program('xsltproc') + custom_target('manfile-gnome-software', + input: 'gnome-software.xml', + output: 'gnome-software.1', + install: true, + install_dir: join_paths(get_option('mandir'), 'man1'), + command: [ + xsltproc, + '--nonet', + '--stringparam', 'man.output.quietly', '1', + '--stringparam', 'funcsynopsis.style', 'ansi', + '--stringparam', 'man.th.extra1.suppress', '1', + '--stringparam', 'man.authors.section.enabled', '0', + '--stringparam', 'man.copyright.section.enabled', '0', + '-o', '@OUTPUT@', + 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl', + '@INPUT@' + ] + ) +endif + +if get_option('packagekit') + # replace @bindir@ + configure_file( + input : 'org.freedesktop.PackageKit.service.in', + output : 'org.freedesktop.PackageKit.service', + install_dir: join_paths(get_option('datadir'), 'dbus-1', 'services'), + configuration : cdata + ) +endif + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', 'data') + '"'] + e = executable( + 'gs-self-test-src', + compiled_schemas, + sources : [ + 'gs-css.c', + 'gs-common.c', + 'gs-content-rating.c', + 'gs-self-test.c', + ], + include_directories : [ + include_directories('..'), + include_directories('../lib'), + ], + dependencies : [ + appstream_glib, + gio_unix, + glib, + gmodule, + goa, + gtk, + json_glib, + libm, + libsoup, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs + ) + test('gs-self-test-src', e, suite: ['plugins', 'src'], env: test_env) +endif diff --git a/src/org.freedesktop.PackageKit.Modify2.xml b/src/org.freedesktop.PackageKit.Modify2.xml new file mode 100644 index 0000000..1e8fb7d --- /dev/null +++ b/src/org.freedesktop.PackageKit.Modify2.xml @@ -0,0 +1,560 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [ + <!ENTITY ERROR_GENERAL "org.freedesktop.PackageKit.Denied"> +]> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + + <interface name="org.freedesktop.PackageKit.Modify2"> + <doc:doc> + <doc:description> + <doc:para> + The interface used for modifying the package database (version 2). + </doc:para> + </doc:description> + </doc:doc> + + <!--*****************************************************************************************--> + <method name="InstallPackageFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs local package files or service packs. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An interaction mode that specifies which UI elements should be shown + or hidden different from the user default, e.g. + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,show-progress</doc:tt>. + The show options are: + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt>. + The hide options are: + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,hide-progress,hide-finished,hide-warning</doc:tt>. + Convenience options such as: + <doc:tt>never</doc:tt>, <doc:tt>defaults</doc:tt> or <doc:tt>always</doc:tt>. + are also available. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallProvideFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs packages to provide files. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallPackageNames"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs packages from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="packages" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of package names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallMimeTypes"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs mimetype handlers from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="mime_types" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of mime types, e.g. <doc:tt>text/plain</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallFontconfigResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs fontconfig resources (usually fonts) from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of font descriptors from fontconfig, e.g. <doc:tt>:lang=mn</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallGStreamerResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs GStreamer fontconfig resources (usually codecs) from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of codecs descriptors from <doc:tt>pk-gstreamer-install</doc:tt>, e.g. + <doc:tt>Advanced Streaming Format (ASF) demuxer|decoder-video/x-ms-asf</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs resources of a given type from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="s" name="type" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The type of resource to request, e.g. <doc:tt>plasma-service</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of resource descriptors + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="RemovePackageByFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Removes packages that provide the given local files. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An interaction mode that specifies which UI elements should be shown + or hidden different from the user default, e.g. + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,show-progress</doc:tt>. + The show options are: + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt>. + The hide options are: + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,hide-progress,hide-finished,hide-warning</doc:tt>. + Convenience options such as: + <doc:tt>never</doc:tt>, <doc:tt>defaults</doc:tt> or <doc:tt>always</doc:tt>. + are also available. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <method name="InstallPrinterDrivers"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs printer drivers from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of printer model descriptors in IEEE 1284 + Device ID format, + e.g. <doc:tt>MFG:Hewlett-Packard;MDL:HP LaserJet + 6MP;</doc:tt>. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="desktop_id" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The desktop file ID of the calling application. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="a{sv}" name="platform_data" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + Additional platform specific data, in the form of GVariant + dictionary mapping strings to variants. As the name indicates, + the platform data may vary depending on the operating system. + Currently recognized keys are: + <doc:tt>desktop-startup-id</doc:tt>: startup notification + identifier that is used to transfer focus to the software installer application; + if the startup notification id is not available, this can be just "_TIMEeventtime", + where eventtime is the time stamp from the event triggering the call. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <property name="DisplayName" type="s" access="read"> + <doc:doc> + <doc:description> + <doc:para> + Translated, human readable name of the program implementing the interface, e.g. 'Software' for gnome-software. + </doc:para> + </doc:description> + </doc:doc> + </property> + + </interface> +</node> diff --git a/src/org.freedesktop.PackageKit.service.in b/src/org.freedesktop.PackageKit.service.in new file mode 100644 index 0000000..54c0466 --- /dev/null +++ b/src/org.freedesktop.PackageKit.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.PackageKit +Exec=@bindir@/gnome-software --gapplication-service diff --git a/src/org.freedesktop.PackageKit.xml b/src/org.freedesktop.PackageKit.xml new file mode 100644 index 0000000..c70cac3 --- /dev/null +++ b/src/org.freedesktop.PackageKit.xml @@ -0,0 +1,506 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [ + <!ENTITY ERROR_GENERAL "org.freedesktop.PackageKit.Denied"> +]> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + + <interface name="org.freedesktop.PackageKit.Query"> + <doc:doc> + <doc:description> + <doc:para> + The interface used for querying the package database. + </doc:para> + </doc:description> + </doc:doc> + + <!--*****************************************************************************************--> + <method name="IsInstalled"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Finds out if the package is installed. + </doc:para> + </doc:description> + </doc:doc> + <arg type="s" name="package_name" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + A package name, e.g. <doc:tt>hal-info</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>timeout=10</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="b" name="installed" direction="out"> + <doc:doc> + <doc:summary> + <doc:para> + If the package is installed. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="SearchFile"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Finds the package name for an installed or available file + </doc:para> + </doc:description> + </doc:doc> + <arg type="s" name="file_name" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + A package name, e.g. <doc:tt>/usr/share/help/gimp/index.html</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>timeout=10</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="b" name="installed" direction="out"> + <doc:doc> + <doc:summary> + <doc:para> + If the package is installed. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="package_name" direction="out"> + <doc:doc> + <doc:summary> + <doc:para> + The package name of the file, e.g. <doc:tt>hal-info</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + </interface> + + <!-- ######################################################################################### --> + <interface name="org.freedesktop.PackageKit.Modify"> + <doc:doc> + <doc:description> + <doc:para> + The interface used for modifying the package database. + </doc:para> + </doc:description> + </doc:doc> + + <!--*****************************************************************************************--> + <method name="InstallPackageFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs local package files or service packs. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An interaction mode that specifies which UI elements should be shown + or hidden different from the user default, e.g. + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,show-progress</doc:tt>. + The show options are: + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt>. + The hide options are: + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,hide-progress,hide-finished,hide-warning</doc:tt>. + Convenience options such as: + <doc:tt>never</doc:tt>, <doc:tt>defaults</doc:tt> or <doc:tt>always</doc:tt>. + are also available. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallProvideFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs packages to provide files. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallPackageNames"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs packages from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="packages" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of package names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallMimeTypes"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs mimetype handlers from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="mime_types" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of mime types, e.g. <doc:tt>text/plain</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallFontconfigResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs fontconfig resources (usually fonts) from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of font descriptors from fontconfig, e.g. <doc:tt>:lang=mn</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallGStreamerResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs GStreamer fontconfig resources (usually codecs) from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of codecs descriptors from <doc:tt>pk-gstreamer-install</doc:tt>, e.g. + <doc:tt>Advanced Streaming Format (ASF) demuxer|decoder-video/x-ms-asf</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="InstallResources"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs resources of a given type from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="type" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The type of resource to request, e.g. <doc:tt>plasma-service</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of resource descriptors + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <!--*****************************************************************************************--> + <method name="RemovePackageByFiles"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Removes packages that provide the given local files. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="files" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of file names. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An interaction mode that specifies which UI elements should be shown + or hidden different from the user default, e.g. + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,show-progress</doc:tt>. + The show options are: + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt>. + The hide options are: + <doc:tt>hide-confirm-search,hide-confirm-deps,hide-confirm-install,hide-progress,hide-finished,hide-warning</doc:tt>. + Convenience options such as: + <doc:tt>never</doc:tt>, <doc:tt>defaults</doc:tt> or <doc:tt>always</doc:tt>. + are also available. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + + <method name="InstallPrinterDrivers"> + <annotation name="org.freedesktop.DBus.GLib.Async" value=""/> + <doc:doc> + <doc:description> + <doc:para> + Installs printer drivers from a configured software source. + </doc:para> + </doc:description> + </doc:doc> + <arg type="u" name="xid" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + The X window handle ID, used for focus stealing prevention and setting modality. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="as" name="resources" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An array of printer model descriptors in IEEE 1284 + Device ID format, + e.g. <doc:tt>MFG:Hewlett-Packard;MDL:HP LaserJet + 6MP;</doc:tt>. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <arg type="s" name="interaction" direction="in"> + <doc:doc> + <doc:summary> + <doc:para> + An optional interaction mode, e.g. + <doc:tt>show-confirm-search,show-confirm-deps,show-confirm-install,show-progress,show-finished,show-warning</doc:tt> + </doc:para> + </doc:summary> + </doc:doc> + </arg> + </method> + </interface> +</node> + diff --git a/src/org.gnome.Software-search-provider.ini b/src/org.gnome.Software-search-provider.ini new file mode 100644 index 0000000..7d8f809 --- /dev/null +++ b/src/org.gnome.Software-search-provider.ini @@ -0,0 +1,5 @@ +[Shell Search Provider] +DesktopId=org.gnome.Software.desktop +BusName=org.gnome.Software +ObjectPath=/org/gnome/Software/SearchProvider +Version=2 diff --git a/src/org.gnome.Software.desktop.in b/src/org.gnome.Software.desktop.in new file mode 100644 index 0000000..dde6bad --- /dev/null +++ b/src/org.gnome.Software.desktop.in @@ -0,0 +1,18 @@ +[Desktop Entry] +Name=Software +Comment=Add, remove or update software on this computer +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=org.gnome.Software +Exec=gnome-software %U +Terminal=false +Type=Application +Categories=GNOME;GTK;System;PackageManager; +# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store; +StartupNotify=true +MimeType=x-scheme-handler/appstream;x-scheme-handler/apt;x-scheme-handler/snap; +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=gnome-software +X-GNOME-Bugzilla-Component=gnome-software +X-GNOME-UsesNotifications=true +DBusActivatable=true diff --git a/src/org.gnome.Software.service.in b/src/org.gnome.Software.service.in new file mode 100644 index 0000000..ed3a53d --- /dev/null +++ b/src/org.gnome.Software.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Software +Exec=@bindir@/gnome-software --gapplication-service diff --git a/src/shell-search-provider-dbus-interfaces.xml b/src/shell-search-provider-dbus-interfaces.xml new file mode 100644 index 0000000..f6840e2 --- /dev/null +++ b/src/shell-search-provider-dbus-interfaces.xml @@ -0,0 +1,44 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> + +<!-- + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, see <http://www.gnu.org/licenses/>. +--> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <interface name='org.gnome.Shell.SearchProvider2'> + <method name='GetInitialResultSet'> + <arg type='as' name='Terms' direction='in' /> + <arg type='as' name='Results' direction='out' /> + </method> + <method name = 'GetSubsearchResultSet'> + <arg type='as' name='PreviousResults' direction='in' /> + <arg type='as' name='Terms' direction='in' /> + <arg type='as' name='Results' direction='out' /> + </method> + <method name = 'GetResultMetas'> + <arg type='as' name='Results' direction='in' /> + <arg type='aa{sv}' name='Metas' direction='out' /> + </method> + <method name = 'ActivateResult'> + <arg type='s' name='Result' direction='in' /> + <arg type='as' name='Terms' direction='in' /> + <arg type='u' name='Timestamp' direction='in' /> + </method> + <method name = 'LaunchSearch'> + <arg type='as' name='Terms' direction='in' /> + <arg type='u' name='Timestamp' direction='in' /> + </method> + </interface> +</node> |