diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e8ff3cdf5..724d8afe5 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -20,7 +20,7 @@ jobs: run: | python -m pip install --upgrade pip sudo apt-get update - sudo apt install -y meson gettext itstool libgirepository1.0-dev gir1.2-gtk-4.0 libgtksourceview-5-dev + sudo apt install -y meson gettext itstool libgirepository1.0-dev gir1.2-gtk-4.0 libgtksourceview-5-dev libportal-dev pip install --user -e git+https://github.com/getting-things-gnome/liblarch.git#egg=liblarch pip install --user pytest pycairo PyGObject caldav lxml - name: Build and install GTG diff --git a/GTG/__init__.py b/GTG/__init__.py index c05612362..44b2a5f76 100644 --- a/GTG/__init__.py +++ b/GTG/__init__.py @@ -24,3 +24,4 @@ def gi_version_requires(): gi.require_version('Gdk', '4.0') gi.require_version('Gtk', '4.0') gi.require_version('GtkSource', '5') + gi.require_version('Xdp', '1.0') diff --git a/GTG/core/info.py.in b/GTG/core/info.py.in index 7b82ce63d..63c90b672 100644 --- a/GTG/core/info.py.in +++ b/GTG/core/info.py.in @@ -30,41 +30,60 @@ from gettext import gettext as _ # These variables get used by main_window.py to set the About dialog's metadata. # Translator credits are set dynamically in main_window.py, not hardcoded. NAME = "Getting Things GNOME!" +AUTHORS = _("GTG contributors") +COPYRIGHT = _(f"Copyright © 2008-%d {AUTHORS}") \ + % date.today().year SHORT_DESCRIPTION = _("""A personal productivity tool for GNOME, inspired by the GTD methodology.""") # A manual line break looks better in the About dialog. URL = "https://wiki.gnome.org/Apps/GTG" -TRANSLATE_URL = "https://github.com/getting-things-gnome/gtg/" +CHAT_URL = "https://matrix.to/#/#gtg:gnome.org" +SOURCE_CODE_URL = "https://github.com/getting-things-gnome/gtg/" +OPENHUB_URL = "https://www.openhub.net/p/gtg/contributors" REPORT_BUG_URL = "https://github.com/getting-things-gnome/gtg/issues/" EMAIL = "gtg-contributors@lists.launchpad.net" VERSION = '@VCS_TAG@' -AUTHORS_MAINTAINERS = """ -• Diego Garcia Gangl -• Jean-François Fortin Tam -""" +AUTHORS_MAINTAINERS = [ + "Diego Garcia Gangl", + "Jean-François Fortin Tam", +] + # Per-release stats generated as per the "release process and checklist.md" file. # Including contributors with 2 or more commits. -# No need to add extra line breaks between commas, Python/GTK handles them. -# Don't indend lines inside a multi-line string, or it'll show in the About dialog. -AUTHORS_RELEASE_CONTRIBUTORS = """ -• "Neui" -• Mohieddine Drissi -• "odoood" -• Diego Garcia Gangl -• Jean-François Fortin Tam -• Jacob Anderson -• Raidro Manchester -• Daniel Koć -• François Schmidts -• Sebastian Grabowski -• Fridolin Weisser -• Tommy Priest -• Laurent Combe -• Smitty -• Tiziana Sellitto -• "unsupported-transceiver" -""" +AUTHORS_RELEASE_CONTRIBUTORS = [ + "Neui", + "Mohieddine Drissi", + "“odoood”", + "Diego Garcia Gangl", + "Jean-François Fortin Tam", + "Jacob Anderson", + "Raidro Manchester", + "Daniel Koć", + "François Schmidts", + "Sebastian Grabowski", + "Fridolin Weisser", + "Tommy Priest", + "Laurent Combe", + "Smitty", + "Tiziana Sellitto", + "“unsupported-transceiver”", +] + +ARTISTS = [ + "Diego Garcia Gangl (2021 logo)", + "Tobias Bernard (2021 logo)", + "Kalle Persson (2009 logo)", + "Bertrand Rousseau (UX)", + "Jean-François Fortin Tam (UX)" +] + +DOCUMENTERS = [ + "Danielle Vansia", + "Radina Matic", + "Jean-François Fortin Tam" +] -ARTISTS = ["Diego Garcia Gangl (2021 logo)", "Tobias Bernard (2021 logo)", "Kalle Persson (2009 logo)", "Bertrand Rousseau (UX)", "Jean-François Fortin Tam (UX)"] +AUTHORS_MAINTAINERS.sort() +AUTHORS_RELEASE_CONTRIBUTORS.sort() ARTISTS.sort() -DOCUMENTERS = [ "Danielle Vansia", "Radina Matic", "Jean-François Fortin Tam"] +DOCUMENTERS.sort() diff --git a/GTG/core/meson.build b/GTG/core/meson.build index fc7a65c95..9a4828345 100644 --- a/GTG/core/meson.build +++ b/GTG/core/meson.build @@ -48,6 +48,7 @@ gtg_core_sources = [ 'datastore.py', 'filters.py', 'sorters.py', + 'system_info.py', ] gtg_core_plugin_sources = [ diff --git a/GTG/core/system_info.py b/GTG/core/system_info.py new file mode 100644 index 000000000..de66cf3f4 --- /dev/null +++ b/GTG/core/system_info.py @@ -0,0 +1,117 @@ +# system_info.py: Collect system information +# Copyright (C) 2024 GTG Contributors +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +from datetime import date +import gi +import os +import platform +import importlib + +from GTG.core import info + +from gi.repository import Gdk, Gtk, GObject, GLib, Xdp + + +class SystemInfo: + def get_system_info(self, report: bool = False) -> str: + """ + Get system information based on their + availability and installed version. + """ + self.report = report + + sys_info = "" + sys_info += self.__format_info("GTG", info.VERSION) + + if Xdp.Portal.running_under_flatpak(): + sys_info += self.__format_info("Flatpak", self.__get_flatpak_version()) + else: + sys_info += self.__format_info("Flatpak", "False") + + sys_info += self.__format_info("Snap", Xdp.Portal.running_under_snap()) + sys_info += self.__format_info("Display Name", Gdk.Display.get_default().get_name()) + sys_info += self.__format_info("Desktop", os.environ.get("XDG_CURRENT_DESKTOP")) + + sys_info += "\n" + sys_info += self.__format_info("lxml", self.__get_python_module("lxml")) + sys_info += self.__format_info("caldav", self.__get_python_module("caldav")) + sys_info += self.__format_info("liblarch", self.__get_python_module("liblarch")) + sys_info += self.__format_info("Cheetah3", self.__get_python_module("Cheetah")) + sys_info += self.__format_info("dbus-python", self.__get_python_module("dbus")) + sys_info += self.__format_info("pdflatex", self.__get_python_module("pdflatex")) + sys_info += self.__format_info("pypdftk", self.__get_python_module("pdftk")) + + # Only display OS info when user isn't running as Flatpak/Snap + if not Xdp.Portal.running_under_sandbox(): + sys_info += "\n" + sys_info += self.__format_info("OS", GLib.get_os_info("PRETTY_NAME")) + + sys_info += self.__format_info( + "Python", f"{platform.python_implementation()} {platform.python_version()}" + ) + + sys_info += self.__format_info("GLib", self.__version_to_string(GLib.glib_version)) + sys_info += self.__format_info("PyGLib", self.__version_to_string(GLib.pyglib_version)) + + sys_info += self.__format_info( + "PyGObject", self.__version_to_string(GObject.pygobject_version) + ) + + sys_info += self.__format_info("GTK", self.__version_to_string(self.__get_gtk_version())) + + return sys_info + + + def __version_to_string(self, version: tuple) -> str: + """ + Convert version tuple (major, micro, minor) + version to string (major.micro.minor). + """ + return ".".join(map(str, version)) + + + def __format_info(self, lib: str, getter) -> str: + """ + Pretty-format library and availability + """ + if self.report: + return f"**{lib}:** {getter}\n" + else: + return f"{lib}: {getter}\n" + + + def __get_flatpak_version(self) -> str: + """Get Flatpak version.""" + with open("/.flatpak-info") as flatpak_info: + for line in flatpak_info: + if line.startswith("flatpak-version"): + flatpak_version = line.split("=")[1].strip() + return flatpak_version + + + def __get_gtk_version(self) -> str: + """Get GTK version.""" + return ( + Gtk.get_major_version(), + Gtk.get_micro_version(), + Gtk.get_minor_version(), + ) + + + def __get_python_module(self, module: str) -> bool: + """Check if Python module is installed.""" + return bool(importlib.util.find_spec(module)) diff --git a/GTG/gtk/browser/main_window.py b/GTG/gtk/browser/main_window.py index 19ef4aa0c..5bb567057 100644 --- a/GTG/gtk/browser/main_window.py +++ b/GTG/gtk/browser/main_window.py @@ -25,8 +25,11 @@ from typing import Optional from gi.repository import GObject, Gtk, Gdk, Gio, GLib +from webbrowser import open as openurl +from textwrap import dedent from GTG.core import info +from GTG.core.system_info import SystemInfo from GTG.backends.backend_signals import BackendSignals from GTG.core.dirs import ICONS_DIR from GTG.core.search import parse_search_query, InvalidQuery @@ -319,51 +322,75 @@ def _init_about_dialog(self): """ Show the about dialog """ - # These lines should be in info.py, but due to their dynamic nature - # there'd be no way to show them translated in Gtk's About dialog: - - translated_copyright = _("Copyright © 2008-%d the GTG contributors.") \ - % datetime.date.today().year - - ohstats_url = 'OpenHub' - ghstats_url = \ - 'GitHub' - - UNITED_AUTHORS_OF_GTGETTON = [ - # GTK prefixes the first line with "Created by ", - # but we can't split the string because it would cause trouble for some languages. - _("GTG was made by many contributors around the world."), - _("The GTG project is maintained/administered by:"), - info.AUTHORS_MAINTAINERS, - _("This release was brought to you by the efforts of these people:"), - info.AUTHORS_RELEASE_CONTRIBUTORS, - _("Many others contributed to GTG over the years.\n" \ - "You can see them on {OH_stats} and {GH_stats}.").format( - OH_stats=ohstats_url, GH_stats=ghstats_url), - "\n"] + + # Create `GtkButton`s and add to size group + def create_uri_button(string=None, uri=None): + btn = Gtk.Button.new_with_mnemonic(string) + btn.connect("clicked", lambda _: openurl(uri)) + btn.set_tooltip_text(uri) + size_group.add_widget(btn) + return btn + + ohstats_url = f'OpenHub' + ghstats_url = 'GitHub' + + UNITED_AUTHORS_OF_GTGETTON = dedent( + _( + """\ + Many others contributed to GTG over the years. + You can find them on {OH_stats} and {GH_stats}.""" + ).format(OH_stats=ohstats_url, GH_stats=ghstats_url) + ) self.about.set_transient_for(self) + self.about.set_modal(True) self.about.set_program_name(info.NAME) - self.about.set_website(info.URL) self.about.set_logo_icon_name(self.app.props.application_id) - self.about.set_website_label(_("GTG website")) self.about.set_version(info.VERSION) # This line translated in info.py works, as it has no strings replacements self.about.set_comments(_(info.SHORT_DESCRIPTION)) - self.about.set_copyright(translated_copyright) + self.about.set_copyright(info.COPYRIGHT) self.about.set_license_type(Gtk.License.GPL_3_0) - self.about.set_authors(UNITED_AUTHORS_OF_GTGETTON) + self.about.set_authors([info.AUTHORS]) self.about.set_artists(info.ARTISTS) self.about.set_documenters(info.DOCUMENTERS) + self.about.set_system_information(SystemInfo().get_system_info()) + + self.about.add_credit_section( + _("Maintained/Administered by"), info.AUTHORS_MAINTAINERS + ) + + authors = info.AUTHORS_RELEASE_CONTRIBUTORS + authors.append(UNITED_AUTHORS_OF_GTGETTON) + + self.about.add_credit_section( + _("Contributed by"), info.AUTHORS_RELEASE_CONTRIBUTORS + ) + # Translators for a particular language should put their names here. - # Please keep the width at 80 chars max, as GTK3's About dialog won't wrap text. + # Please keep the width at 80 chars max, as GTK4's About dialog won't wrap text. # GtkAboutDialog will detect if “translator-credits” is untranslated and auto-hide the tab. self.about.set_translator_credits(_("translator-credits")) + # Retrieve the `GtkBox` within GtkDialog, + # and create `GtkSizeGroup` to group buttons + about_box = self.about.get_first_child() + size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) + + # Create new `GtkBox` to add button links + uri_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6) + uri_box.set_halign(Gtk.Align.CENTER) + uri_box.append(create_uri_button(_("_Website"), info.URL)) + uri_box.append(create_uri_button(_("_Dev Chatroom"), info.CHAT_URL)) + uri_box.append(create_uri_button(_("_GitHub"), info.SOURCE_CODE_URL)) + + about_box.append(uri_box) + + def _init_signal_connections(self): """ connects signals on UI elements diff --git a/GTG/gtk/errorhandler.py b/GTG/gtk/errorhandler.py index 3d837cd9f..1ee73ec13 100644 --- a/GTG/gtk/errorhandler.py +++ b/GTG/gtk/errorhandler.py @@ -12,6 +12,7 @@ import configparser from GTG.core import info +from GTG.core.system_info import SystemInfo log = logging.getLogger(__name__) @@ -115,60 +116,11 @@ def _update_additional_info(self): if self._context_info is not None: text = text + "**Context:** " + self._context_info + "\n\n" text = text + "```python-traceback\n" + body + "```\n\n" - text = text + _collect_versions() + text = text + SystemInfo().get_system_info(report=True) self._additional_info.get_buffer().set_text(text) -def _collect_versions() -> str: - """ - Collect version information of various components, - and lay it out in markdown format to facilitate bug reports. - """ - def t2v(version_tuple) -> str: - """Version tuple to a string.""" - return '.'.join(map(str, version_tuple)) - python_version = sys.version.replace('\n', ' ') - gtk_version = (Gtk.get_major_version(), - Gtk.get_minor_version(), - Gtk.get_micro_version()) - versions = f"""**Software versions:** -* {info.NAME} {info.VERSION} -* {platform.python_implementation()} {python_version} -* GTK {t2v(gtk_version)}, GLib {t2v(GLib.glib_version)} -* PyGLib {t2v(GLib.pyglib_version)}, PyGObject {t2v(GObject.pygobject_version)} -* {platform.platform()}""" - - flatpak_info = _collect_flatpak_info() - if flatpak_info is not None: - versions += f""" -* Flatpak {flatpak_info.get('Instance.flatpak-version')} -* Flatpak runtime: {flatpak_info.get('Application.runtime')} ({flatpak_info.get('Instance.runtime-commit')}) -* Flatpak app: {flatpak_info.get('Application.name')} ({flatpak_info.get('Instance.app-commit')})""" - - return versions - - -def _collect_flatpak_info() -> Optional[dict]: - """ - Collect all kinds of information easily available when running inside - the flatpak environment. - Returns None if not running in a flatpak. - """ - try: - fconfig = configparser.ConfigParser(interpolation=None) - if fconfig.read('/.flatpak-info') == []: - return None - d = {} - for section in fconfig.sections(): - for option in fconfig[section]: - d[f'{section}.{option}'] = fconfig[section][option] - return d - except Exception as e: # Just in case - log.exception("Exception occurred while trying to collcet flatpak info") - return None - - def _format_exception(exception: Exception) -> str: """Format an exception the python way, as a string.""" return "".join(traceback.format_exception(type(exception), diff --git a/build-aux/org.gnome.GTG.Devel.json b/build-aux/org.gnome.GTG.Devel.json index 3fd261253..80545853f 100644 --- a/build-aux/org.gnome.GTG.Devel.json +++ b/build-aux/org.gnome.GTG.Devel.json @@ -45,6 +45,24 @@ "mkdir ${FLATPAK_DEST}/texlive" ] }, + { + "name": "libportal", + "buildsystem": "meson", + "config-opts": [ + "-Dtests=false", + "-Dbackend-gtk3=disabled", + "-Dbackend-gtk4=enabled", + "-Dbackend-qt5=disabled", + "-Ddocs=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/flatpak/libportal/releases/download/0.7.1/libportal-0.7.1.tar.xz", + "sha256": "297b90b263fad22190a26b8c7e8ea938fe6b18fb936265e588927179920d3805" + } + ] + }, { "name": "gtg", "config-opts" : [