diff options
author | Hendrik Müller <henne90gen@gmail.com> | 2022-11-18 20:13:04 +0100 |
---|---|---|
committer | Niels De Graef <nielsdegraef@gmail.com> | 2023-02-11 06:34:02 +0000 |
commit | 19f37fbe3e5be87fefe79d1fe6f3c66319acb69e (patch) | |
tree | e5bce185478678ea4758c560b26c227043fc524e | |
parent | ac886150ac4533035e8451a2035f05ce412ef2cf (diff) | |
download | gnome-contacts-19f37fbe3e5be87fefe79d1fe6f3c66319acb69e.tar.gz |
contact: Add QR code to share individual contacts
Sharing contacts in an easy and offline way is currently not possible.
Most mobile phones have a camera and are capable of scanning QR codes.
The vCard format is widely used to easily exchange contact information.
A contact can be saved in vCard format into a QR code.
A button with a QR code icon is added next to the "favourite" and "edit"
buttons. When the user presses this button, a dialog opens up, which
shows a QR code containing the current contacts data in vCard format.
-rw-r--r-- | data/contacts.gresource.xml | 2 | ||||
-rw-r--r-- | data/icons/scalable/actions/qr-code-symbolic.svg | 2 | ||||
-rw-r--r-- | data/ui/contacts-main-window.ui | 36 | ||||
-rw-r--r-- | data/ui/contacts-qr-code-dialog.ui | 60 | ||||
-rw-r--r-- | data/ui/style.css | 10 | ||||
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | po/POTFILES.in | 1 | ||||
-rw-r--r-- | src/contacts-main-window.vala | 34 | ||||
-rw-r--r-- | src/contacts-qr-code-dialog.vala | 109 | ||||
-rw-r--r-- | src/meson.build | 2 | ||||
-rw-r--r-- | vapi/custom.vapi | 1 | ||||
-rw-r--r-- | vapi/libqrencode.vapi | 63 |
12 files changed, 300 insertions, 21 deletions
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml index 89c8052..3daf6d6 100644 --- a/data/contacts.gresource.xml +++ b/data/contacts.gresource.xml @@ -11,6 +11,7 @@ <file preprocess="xml-stripblanks">icons/scalable/actions/map-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/note-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/photo-camera-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/scalable/actions/qr-code-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/website-symbolic.svg</file> <file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file> @@ -22,6 +23,7 @@ <file compressed="true" preprocess="xml-stripblanks">ui/contacts-linked-personas-dialog.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/contacts-main-window.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/contacts-preferences-window.ui</file> + <file compressed="true" preprocess="xml-stripblanks">ui/contacts-qr-code-dialog.ui</file> <file compressed="true" preprocess="xml-stripblanks">ui/contacts-setup-window.ui</file> </gresource> </gresources> diff --git a/data/icons/scalable/actions/qr-code-symbolic.svg b/data/icons/scalable/actions/qr-code-symbolic.svg new file mode 100644 index 0000000..8096300 --- /dev/null +++ b/data/icons/scalable/actions/qr-code-symbolic.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 1 1 v 5 h 5 v -5 z m 7 0 v 1 h 1 v -1 z m 0 1 h -1 v 1 h 1 z m 0 1 v 1 h -1 v 2 h 2 v -3 z m 2 -2 v 5 h 5 v -5 z m -7 2 h 1 v 1 h -1 z m 9 0 h 1 v 1 h -1 z m -11 4 v 1 h 1 v -1 z m 1 1 v 1 h 1 v -1 z m 1 0 h 1 v 1 h 2 v -2 h -3 z m 4 -1 v 4 h 1 v -1 h 2 v -3 z m 3 3 v 1 h 1 v -1 z m 1 0 h 1 v -1 h -1 z m 1 -1 h 1 v 1 h 1 v -2 h 1 v -1 h -3 z m 1 1 h -1 v 1 h 1 z m -3 1 h -1 v 1 h 1 z m 0 1 v 1 h 1 v -1 z m -1 0 h -2 v 3 h 1 v -1 h 1 z m 0 2 v 1 h 1 v -1 z m -1 -6 h 1 v 1 h -1 z m -7 2 v 5 h 5 v -5 z m 13 1 v 1 h -2 v 3 h 1 v -1 h 1 v -1 h 1 v -2 z m 0 3 v 1 h 1 v -1 z m -11 -2 h 1 v 1 h -1 z m 0 0" fill="#222222"/></svg> diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui index 1b2cd6b..7682ebf 100644 --- a/data/ui/contacts-main-window.ui +++ b/data/ui/contacts-main-window.ui @@ -40,6 +40,25 @@ </section> </menu> + <menu id="contact_hamburger_menu_popover"> + <section> + <item> + <attribute name="action">win.toggle-favorite</attribute> + <attribute name="custom">favorite-toggle</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Share as QR Code</attribute> + <attribute name="action">win.show-contact-qr-code</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Delete Contact</attribute> + <attribute name="action">win.delete-contact</attribute> + </item> + </section> + </menu> + <template class="ContactsMainWindow" parent="AdwApplicationWindow"> <property name="default_width">800</property> <property name="default_height">600</property> @@ -247,23 +266,18 @@ <property name="orientation">horizontal</property> <property name="spacing">6</property> <child> - <object class="GtkToggleButton" id="favorite_button"> - <property name="icon-name">starred-symbolic</property> - <signal name="toggled" handler="on_favorite_button_toggled"/> - </object> - </child> - <child> <object class="GtkButton" id="edit_contact_button"> <property name="icon-name">document-edit-symbolic</property> <property name="action-name">win.edit-contact</property> <property name="tooltip-text" translatable="yes">Edit Contact</property> </object> </child> - <child> - <object class="GtkButton" id="delete_contact_button"> - <property name="icon-name">user-trash-symbolic</property> - <property name="action-name">win.delete-contact</property> - <property name="tooltip-text" translatable="yes">Delete Contact</property> + <child type="end"> + <object class="GtkMenuButton" id="contact_hamburger_menu_button"> + <property name="menu-model">contact_hamburger_menu_popover</property> + <property name="primary">True</property> + <property name="tooltip_text" translatable="yes">More Contact Actions</property> + <property name="icon-name">open-menu-symbolic</property> </object> </child> </object> diff --git a/data/ui/contacts-qr-code-dialog.ui b/data/ui/contacts-qr-code-dialog.ui new file mode 100644 index 0000000..51d4929 --- /dev/null +++ b/data/ui/contacts-qr-code-dialog.ui @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ContactsQrCodeDialog" parent="AdwPreferencesWindow"> + <property name="modal">True</property> + <property name="default-width">400</property> + <property name="default-height">600</property> + <property name="height-request">600</property> + <property name="destroy-with-parent">True</property> + <property name="search-enabled">False</property> + <property name="can-navigate-back">False</property> + <property name="title" translatable="yes">Share Contact</property> + + <child> + <object class="AdwPreferencesPage"> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkPicture" id="qr_image"> + <property name="alternative-text">QR Code</property> + <property name="halign">center</property> + <property name="width-request">300</property> + <property name="height-request">300</property> + <style> + <class name="frame"/> + <class name="contacts-qr-code-dialog-qr-image"/> + </style> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkLabel" id="qr_title"> + <property name="label" translatable="yes">Scan to Save</property> + <property name="css-classes">title-1</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkLabel" id="qr_subtitle"> + <property name="halign">center</property> + <property name="wrap">true</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/data/ui/style.css b/data/ui/style.css index 63be3c8..f33ee8d 100644 --- a/data/ui/style.css +++ b/data/ui/style.css @@ -88,3 +88,13 @@ flowboxchild.circular { .contacts-editor-birthday { margin: 12px; } + +.contacts-qr-code-dialog-qr-image { + background: #fff; + padding: 12px; +} + +.favorite-button { + font-weight: normal; +} + diff --git a/meson.build b/meson.build index 8ea040f..339e5d2 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,7 @@ libportal_dep = dependency('libportal-gtk4', version: '>= 0.6') # Cheese # cheese_dep = dependency('cheese', required: get_option('cheese')) # cheese_gtk_dep = dependency('cheese-gtk', version: '>= 3.3.91', required: get_option('cheese')) +libqrencode_dep = dependency('libqrencode', version: '>=4.1.1') # gnome-online-accounts if get_option('goa') goa_dep = dependency('goa-1.0') diff --git a/po/POTFILES.in b/po/POTFILES.in index ead6b22..5a8c2a2 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,6 +11,7 @@ data/ui/contacts-linked-personas-dialog.ui data/ui/contacts-link-suggestion-grid.ui data/ui/contacts-main-window.ui data/ui/contacts-preferences-window.ui +data/ui/contacts-qr-code-dialog.ui data/ui/contacts-setup-window.ui src/contacts-accounts-list.vala src/contacts-app.vala diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala index 0d62184..ca1181f 100644 --- a/src/contacts-main-window.vala +++ b/src/contacts-main-window.vala @@ -25,10 +25,11 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { { "new-contact", new_contact }, { "edit-contact", edit_contact }, { "stop-editing-contact", stop_editing_contact, "b" }, + { "toggle-favorite", toggle_favorite }, { "link-marked-contacts", link_marked_contacts }, { "delete-marked-contacts", delete_marked_contacts }, { "export-marked-contacts", export_marked_contacts }, - // { "share-contact", share_contact }, + { "show-contact-qr-code", show_contact_qr_code }, { "unlink-contact", unlink_contact }, { "delete-contact", delete_contact }, { "sort-on", null, "s", "'surname'", sort_on_changed }, @@ -68,9 +69,11 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.MenuButton primary_menu_button; [GtkChild] - private unowned Gtk.Box contact_sheet_buttons; + private unowned Gtk.MenuButton contact_hamburger_menu_button; + private unowned Gtk.PopoverMenu contact_hamburger_popover_menu; + private Gtk.Button favorite_button; [GtkChild] - private unowned Gtk.ToggleButton favorite_button; + private unowned Gtk.Box contact_sheet_buttons; private bool ignore_favorite_button_toggled; [GtkChild] private unowned Gtk.Button add_button; @@ -135,6 +138,12 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { unowned var sort_key = this.settings.sort_on_surname? "surname" : "firstname"; var sort_action = (SimpleAction) this.lookup_action ("sort-on"); sort_action.set_state (new Variant.string (sort_key)); + + contact_hamburger_popover_menu = (Gtk.PopoverMenu) contact_hamburger_menu_button.get_popover (); + favorite_button = new Gtk.Button.with_label (_("Mark as Favorite")); + favorite_button.set_action_name ("win.toggle-favorite"); + favorite_button.set_css_classes ({"flat", "favorite-button"}); + contact_hamburger_popover_menu.add_child (favorite_button, "favorite-toggle"); } private void restore_window_state () { @@ -253,8 +262,13 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { this.contact_pane.edit_contact (); } - [GtkCallback] - private void on_favorite_button_toggled (Gtk.ToggleButton button) { + private void show_contact_qr_code (GLib.SimpleAction action, GLib.Variant? parameter) { + unowned var selected = this.store.get_selected_contact (); + var dialog = new QrCodeDialog.for_contact (selected, get_root () as Gtk.Window); + dialog.show (); + } + + private void toggle_favorite (GLib.SimpleAction action, GLib.Variant? parameter) { // Don't change the contact being favorite while switching between the two of them if (this.ignore_favorite_button_toggled) return; @@ -263,6 +277,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { return_if_fail (selected != null); selected.is_favourite = !selected.is_favourite; + + this.state = UiState.NORMAL; + this.contact_hamburger_popover_menu.popdown (); } [GtkCallback] @@ -445,13 +462,10 @@ public class Contacts.MainWindow : Adw.ApplicationWindow { // clearing right_header this.right_header.title_widget = new Adw.WindowTitle ("", ""); if (selected != null) { - this.ignore_favorite_button_toggled = true; - this.favorite_button.active = selected.is_favourite; - this.ignore_favorite_button_toggled = false; if (selected.is_favourite) - this.favorite_button.tooltip_text = _("Unmark as favorite"); + this.favorite_button.set_label (_("Unmark as Favorite")); else - this.favorite_button.tooltip_text = _("Mark as favorite"); + this.favorite_button.set_label (_("Mark as Favorite")); } this.state = UiState.SHOWING; } diff --git a/src/contacts-qr-code-dialog.vala b/src/contacts-qr-code-dialog.vala new file mode 100644 index 0000000..02c0539 --- /dev/null +++ b/src/contacts-qr-code-dialog.vala @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 Elias Entrup <elias-git@flump.de> + * + * 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 2 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 <http://www.gnu.org/licenses/>. + */ + +using Folks; +using GLib; + +[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-qr-code-dialog.ui")] +public class Contacts.QrCodeDialog : Adw.PreferencesWindow { + + [GtkChild] + private unowned Gtk.Picture qr_image; + + [GtkChild] + private unowned Gtk.Label qr_subtitle; + + public QrCodeDialog.for_contact (Individual individual, Gtk.Window? parent = null) { + Object (transient_for: parent); + + var subtitle = GLib.Markup.printf_escaped (_("Scan the QR code to save the contact <b>%s</b>."), + individual.display_name); + this.qr_subtitle.set_markup (subtitle); + + var individuals = new Gee.ArrayList<Individual> (); + individuals.add (individual); + + var stringstream = new GLib.MemoryOutputStream.resizable (); + var op = new Io.VCardExportOperation (individuals, stringstream); + op.execute.begin ((obj, res) => { + try { + op.execute.end (res); + uint8[] chars = {0}; + stringstream.write (chars); + stringstream.close (); + } catch (Error e) { + warning ("ERROR: %s", e.message); + } + + var content = (string) stringstream.steal_data (); + int QR_IMAGE_SIZE = 300; + var scale = this.qr_image.get_scale_factor (); + create_qr_code (content, QR_IMAGE_SIZE * scale); + }); + } + + private void create_qr_code (string content, int size) { + if (content == "") { + warning ("Failed to create QR code: no content"); + return; + } + + var result = new QRencode.QRcode.encodeString (content, + 0, + QRencode.EcLevel.M, + QRencode.Mode.B8, + 1); + if (result == null) { + warning ("Failed to create QR code: libqrencode error"); + return; + } + + var qr_size = result.width; + var pixel_size = (int) double.max (1, size / qr_size); + var total_size = qr_size * pixel_size; + var BYTES_PER_R8G8B8 = 3; + var qr_matrix = new GLib.ByteArray.sized ((uint)(total_size * total_size * pixel_size * BYTES_PER_R8G8B8)); + + for (var column = 0; column < total_size; column++) { + for (var i = 0; i < pixel_size; i++) { + for (var row = 0; row < total_size / pixel_size; row++) { + if ((result.data[qr_size*row + column] & 0x01) > 0) { + fill_pixel (qr_matrix, 0x00, pixel_size); + } else { + fill_pixel (qr_matrix, 0xff, pixel_size); + } + } + } + } + + var bytes = ByteArray.free_to_bytes (qr_matrix); + var paintable = new Gdk.MemoryTexture (total_size, total_size, + Gdk.MemoryFormat.R8G8B8, + bytes, + total_size * BYTES_PER_R8G8B8); + this.qr_image.set_paintable (paintable); + } + + private void fill_pixel (GLib.ByteArray array, uint8 val, int pixel_size) { + for (uint i = 0; i < pixel_size; i++) { + array.append ({val}); // R + array.append ({val}); // G + array.append ({val}); // B + } + } +} + diff --git a/src/meson.build b/src/meson.build index 87a9fd0..6f680e8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -59,6 +59,7 @@ contacts_deps = [ # libedataserverui, libportal_dep, math, + libqrencode_dep, ] if get_option('goa') @@ -94,6 +95,7 @@ contacts_vala_sources = files( 'contacts-link-suggestion-grid.vala', 'contacts-linked-personas-dialog.vala', 'contacts-main-window.vala', + 'contacts-qr-code-dialog.vala', 'contacts-preferences-window.vala', 'contacts-settings.vala', 'contacts-setup-window.vala', diff --git a/vapi/custom.vapi b/vapi/custom.vapi index 5d1484d..9aee3db 100644 --- a/vapi/custom.vapi +++ b/vapi/custom.vapi @@ -9,3 +9,4 @@ namespace Cc { public Gdk.Pixbuf create_pixbuf (); } } + diff --git a/vapi/libqrencode.vapi b/vapi/libqrencode.vapi new file mode 100644 index 0000000..2b8f104 --- /dev/null +++ b/vapi/libqrencode.vapi @@ -0,0 +1,63 @@ +/* qrencode.vapi + * + * Copyright (C) 2015 Ignacio Casal Quinteiro + * + * 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.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * As a special exception, if you use inline functions from this file, + * this file does not by itself cause the resulting executable to be + * covered by the GNU Lesser General Public License. + */ +namespace QRencode { + [CCode (cheader_filename = "qrencode.h", cname = "QRcode", unref_function = "QRcode_free")] + public class QRcode { + [CCode (cname = "QRcode_encodeString")] + public QRcode.encodeString(string digits, int version, EcLevel level, Mode hint, int casesensitive); + + public int version; + public int width; + [CCode (array_length = false)] + public uint8[] data; + } + + [CCode (cheader_filename = "qrencode.h", cname="QRencLevel")] + public enum EcLevel { + [CCode (cname="QR_ECLEVEL_L")] + L, + [CCode (cname="QR_ECLEVEL_M")] + M, + [CCode (cname="QR_ECLEVEL_Q")] + Q, + [CCode (cname="QR_ECLEVEL_H")] + H + } + + [CCode (cheader_filename = "qrencode.h", cname="QRencodeMode")] + public enum Mode { + [CCode (cname="QR_MODE_NUL")] + NUL, + [CCode (cname="QR_MODE_NUM")] + NUM, + [CCode (cname="QR_MODE_AN")] + AN, + [CCode (cname="QR_MODE_8")] + B8, + [CCode (cname="QR_MODE_KANJI")] + KANJI, + [CCode (cname="QR_MODE_STRUCTURE")] + STRUCTURE + } +} |