diff --git a/data/Application.css b/data/Application.css index 0d25913..5dbed3e 100644 --- a/data/Application.css +++ b/data/Application.css @@ -20,10 +20,11 @@ /* Picked: #2c3944d9 Base from wpkelso: #33434eff - -@define-color accent_color #33434eff; */ +@define-color accent_color #33434e; + + /* Used in the OrientationsBox for the preview icon */ .rotated * { -gtk-icon-transform: rotate(90deg); @@ -50,4 +51,38 @@ Base from wpkelso: #33434eff /* Pretend the log textview is a console */ .console { background-color: #002B36; +} + +/* Used for labels in the popover */ +.bold { + font-weight: bold; +} + +/* Font zooms */ +.s20 {font-size: 20%;} +.s40 {font-size: 40%;} +.s60 {font-size: 60%;} +.s80 {font-size: 80%;} +.s100 {font-size: 100%;} +.s120 {font-size: 120%;} +.s140 {font-size: 140%;} +.s160 {font-size: 160%;} +.s180 {font-size: 180%;} +.s200 {font-size: 200%;} +.s220 {font-size: 220%;} +.s240 {font-size: 240%;} +.s260 {font-size: 260%;} +.s280 {font-size: 280%;} +.s300 {font-size: 300%;} +.s320 {font-size: 320%;} +.s340 {font-size: 340%;} +.s360 {font-size: 360%;} +.s380 {font-size: 380%;} +.s400 {font-size: 400%;} + +/* Devel builds get these. Libadwaita has that too, but we do not use it. */ +window.devel { + border-style: solid; + border-width: 3px; + border-color: @warning_color; } \ No newline at end of file diff --git a/data/icons/dev.svg b/data/icons/dev.svg new file mode 100644 index 0000000..2411099 --- /dev/null +++ b/data/icons/dev.svg @@ -0,0 +1,354 @@ + +image/svg+xml diff --git a/data/inscriptions.desktop.in b/data/inscriptions.desktop.in.in similarity index 77% rename from data/inscriptions.desktop.in rename to data/inscriptions.desktop.in.in index 775fb60..50295dc 100644 --- a/data/inscriptions.desktop.in +++ b/data/inscriptions.desktop.in.in @@ -2,11 +2,11 @@ Version=1.0 Type=Application -Name=Inscriptions +Name=@APP_NAME@ GenericName=Text translator Comment=Translate text elegantly -Icon=io.github.elly_code.inscriptions -Exec=io.github.elly_code.inscriptions %U +Icon=@APP_ID@ +Exec=@APP_ID@ %U Categories=Office;Education;GTK; Keywords=text;translate;translation;translator;deepl;NMT;lang;localize;l10n;i18n;localization;internationalization; diff --git a/data/inscriptions.gschema.xml b/data/inscriptions.gschema.xml.in similarity index 92% rename from data/inscriptions.gschema.xml rename to data/inscriptions.gschema.xml.in index 828c18c..aeed32f 100644 --- a/data/inscriptions.gschema.xml +++ b/data/inscriptions.gschema.xml.in @@ -1,6 +1,6 @@ - + @@ -8,7 +8,7 @@ - + 320 Most recent window height @@ -49,7 +49,7 @@ context for translations Passed as context parameter to the DeepL API - + 'default' Level of formality For supported languages, how format the output should be diff --git a/data/inscriptions.metainfo.xml.in b/data/inscriptions.metainfo.xml.in.in similarity index 85% rename from data/inscriptions.metainfo.xml.in rename to data/inscriptions.metainfo.xml.in.in index 541bf13..f514ff3 100644 --- a/data/inscriptions.metainfo.xml.in +++ b/data/inscriptions.metainfo.xml.in.in @@ -1,15 +1,15 @@ - io.github.elly_code.inscriptions - io.github.elly_code.inscriptions.desktop - io.github.elly_code.inscriptions + @APP_ID@ + @APP_ID@.desktop + @GETTEXT_PACKAGE@ CC-BY-4.0 GPL-3.0-or-later io.github.elly_code.inscriptions - Inscriptions + @APP_NAME@ Translate text elegantly

A fast, pretty, and ready translation app using DeepL free and paid API

@@ -49,8 +49,8 @@ https://github.com/elly-code/inscriptions - #95a3ab - #0e141f + #3557082 + #33434e @@ -79,6 +79,16 @@ + + +

Touchups and enhancements

+
    +
  • Minor design changes. Lets do refined shit.
  • +
  • Updated and extended screenshots
  • +
  • Some work into cleaner code
  • +
+
+

🚀 1.0.0 Initial release!

diff --git a/data/meson.build b/data/meson.build index 4b2638b..77a4c82 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,45 +1,104 @@ + +gschema_conf = configuration_data() +gschema_conf.set('APP_ID', app_id) +gschema_file = configure_file( + input: 'inscriptions.gschema.xml.in', + output: '@0@.gschema.xml'.format(app_id), + configuration: gschema_conf, +) + install_data( - 'inscriptions.gschema.xml', + gschema_file, install_dir: get_option('datadir') / 'glib-2.0' / 'schemas', - rename: meson.project_name() + '.gschema.xml' +) + +compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) +test('Validate schema file', +compile_schemas, +args: ['--strict', '--dry-run', meson.current_source_dir()], ) # metainfo confuses windows, and icons are if not windows_build - icon_sizes = ['16', '32', '48', '64', '128'] + # Install the standard icons in standard build + if not get_option('development') + + icon_sizes = ['16', '32', '48', '64', '128'] + + foreach i : icon_sizes + install_data( + 'icons' / 'hicolor' / i + '.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps', + rename: app_id + '.png' + ) + install_data( + 'icons' / 'hicolor@2' / i + '@2.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps', + rename: app_id + '.png' + ) + endforeach - foreach i : icon_sizes - install_data( - 'icons' / 'hicolor' / i + '.png', - install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps', - rename: meson.project_name() + '.png' + # Install the dev icon in development builds + else + install_data( + 'icons' / 'dev.svg', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps', + rename: app_id + '.svg' + ) + endif + + + # Inject some variables into the desktop file before merging in the translations + desktop_conf = configuration_data() + desktop_conf.set('APP_NAME', app_name) + desktop_conf.set('APP_ID', app_id) + desktop_file_in = configure_file( + input: 'inscriptions.desktop.in.in', + output: '@0@.desktop.in'.format(app_id), + configuration: desktop_conf, ) - install_data( - 'icons' / 'hicolor@2' / i + '@2.png', - install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps', - rename: meson.project_name() + '.png' + + desktop_file = i18n.merge_file( + input: desktop_file_in, + output: '@0@.desktop'.format(app_id), + po_dir: meson.project_source_root() / 'po', + type: 'desktop', + install: true, + install_dir: get_option('datadir') / 'applications', ) - endforeach + # Inject some variables into the metainfo file before merging in the translations + appstream_conf = configuration_data() + appstream_conf.set('APP_ID', app_id) + appstream_conf.set('GETTEXT_PACKAGE', meson.project_name()) + appstream_file_in = configure_file( + input: 'inscriptions.metainfo.xml.in.in', + output: '@0@.metainfo.xml.in'.format(app_id), + configuration: appstream_conf, + ) - i18n.merge_file( - input: 'inscriptions.desktop.in', - output: meson.project_name() + '.desktop', + appstream_file = i18n.merge_file( + input: appstream_file_in, + output: '@0@.metainfo.xml'.format(app_id), po_dir: meson.project_source_root() / 'po', - type: 'desktop', install: true, - install_dir: get_option('datadir') / 'applications' + install_dir: get_option('datadir') / 'metainfo', ) - i18n.merge_file( - input: 'inscriptions.metainfo.xml.in', - output: meson.project_name() + '.metainfo.xml', - po_dir: meson.project_source_root() / 'po', - install: true, - install_dir: get_option('datadir') / 'metainfo' + # Test definitions + desktop_utils = find_program('desktop-file-validate', required: false) + if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: [desktop_file]) + endif + + appstreamcli = find_program('appstreamcli', required: false, disabler: true) + test('Validate appstream file', + appstreamcli, + args: ['validate', '--no-net', '--explain', appstream_file], ) + endif \ No newline at end of file diff --git a/io.github.elly_code.inscriptions.devel.yml b/io.github.elly_code.inscriptions.devel.yml new file mode 100644 index 0000000..214ca50 --- /dev/null +++ b/io.github.elly_code.inscriptions.devel.yml @@ -0,0 +1,43 @@ +# +# This is danger +# +id: io.github.elly_code.inscriptions.devel +# elementary SDK is not available on Flathub, so use the elementary BaseApp instead +base: io.elementary.BaseApp +base-version: 'circe-25.08' +runtime: org.gnome.Platform +runtime-version: '49' +sdk: org.gnome.Sdk +command: io.github.elly_code.inscriptions.devel + +tags: ['devel'] +desktop-file-name-suffix: ' (Development)' + +finish-args: + - '--share=ipc' + - '--device=dri' + - '--socket=fallback-x11' + - '--socket=wayland' + # Required for communication with DeepL API + - '--share=network' + +cleanup: + - '/include' + - '/lib/pkgconfig' + - '/man' + - '/share/doc' + - '/share/gtk-doc' + - '/share/man' + - '/share/pkgconfig' + - '/share/installed-tests' + - '*.la' + - '*.a' + +modules: + - name: inscriptions + buildsystem: meson + config-opts: + - -Ddevelopment=true + sources: + - type: dir + path: . \ No newline at end of file diff --git a/meson.build b/meson.build index 261f8e4..97c53e9 100644 --- a/meson.build +++ b/meson.build @@ -2,20 +2,33 @@ project( 'io.github.elly_code.inscriptions', 'vala', 'c', - version: '1.0.0' + version: '1.1.0' ) +app_name = 'Inscriptions' +app_id = meson.project_name() +app_version = meson.project_version() + #================================ # Include Gnome and the translations module gnome = import('gnome') i18n = import('i18n') -# Set our translation domain -add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c') -windows_build = build_machine.system() == 'windows' vala_flags = [] +if get_option('development') + app_id += '.devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() + app_version += '-@0@'.format (vcs_tag) + vala_flags += ['--define', 'DEVEL'] + +endif + +# Set our translation domain +add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (app_id), language:'c') + # Windows needs icons, special arguments, and skip libportal +windows_build = build_machine.system() == 'windows' if windows_build vala_flags += ['--define', 'WINDOWS'] endif @@ -27,7 +40,11 @@ add_project_arguments(vala_flags, language: 'vala') config_data = configuration_data() config_data.set('version', meson.project_version()) config_data.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir')) -config_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) +config_data.set_quoted('GETTEXT_PACKAGE', app_id) +config_data.set_quoted('APP_NAME', app_name) +config_data.set_quoted('APP_ID', app_id) +config_data.set_quoted('APP_VERSION', app_version) + config_file = configure_file( input: 'src/Config.vala.in', output: '@BASENAME@', diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..57ae49d --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('development', type: 'boolean', value: false, description: 'If this is a development build') \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index 0c8f812..17e6b60 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -2,6 +2,7 @@ src/Windows/MainWindow.vala src/Views/TranslationView.vala src/Widgets/HeaderBar.vala src/Views/ErrorView.vala +src/Enums/StatusCode.vala src/Widgets/ErrorBonusBox.vala src/Views/LogView.vala src/Widgets/PopoverWidgets/ApiEntry.vala diff --git a/po/meson.build b/po/meson.build index d148a69..4d77e2e 100644 --- a/po/meson.build +++ b/po/meson.build @@ -1,4 +1,4 @@ -i18n.gettext(meson.project_name(), +i18n.gettext(app_id, args: '--directory=' + meson.project_source_root(), preset: 'glib' ) diff --git a/src/Config.vala.in b/src/Config.vala.in index a3c7bd4..5390828 100644 --- a/src/Config.vala.in +++ b/src/Config.vala.in @@ -1,2 +1,5 @@ public const string GETTEXT_PACKAGE = @GETTEXT_PACKAGE@; -public const string LOCALEDIR = @LOCALEDIR@; \ No newline at end of file +public const string LOCALEDIR = @LOCALEDIR@; +public const string APP_NAME = @APP_NAME@; +public const string APP_ID = @APP_ID@; +public const string APP_VERSION = @APP_VERSION@; \ No newline at end of file diff --git a/src/Constants.vala b/src/Constants.vala index a238eca..868a376 100644 --- a/src/Constants.vala +++ b/src/Constants.vala @@ -6,7 +6,12 @@ namespace Inscriptions { // Alkrjnlgjrt - public const string RDNN = "io.github.elly_code.inscriptions"; +#if DEVEL + public const string RDNN = "io.github.elly_code.inscriptions.devel"; +#else + public const string RDNN = "io.github.elly_code.inscriptions"; +#endif + public const string DONATE_LINK = "https://ko-fi.com/teamcons/tip"; public const string LINK = "https://www.deepl.com/your-account/keys"; public const int DEBOUNCE_INTERVAL = 1250; // ms @@ -94,15 +99,15 @@ namespace Inscriptions { public Lang[] TargetLang () { return { new Lang ("system",_("System language")), - new Lang ("AR",_("Arabic")), new Lang ("BG",_("Bulgarian")), new Lang ("CS",_("Czech")), new Lang ("DA",_("Danish")), new Lang ("DE",_("German")), new Lang ("EL",_("Greek")), - new Lang ("EN",_("English (GB)")), - new Lang ("EN",_("English (US)")), + new Lang ("EN-GB",_("English (GB)")), + new Lang ("EN-US",_("English (US)")), + new Lang ("EO",_("Spanish (All)")), new Lang ("ES",_("Spanish (All)")), new Lang ("ES-419",_("Spanish (Latin American)")), new Lang ("ET",_("Estonian")), diff --git a/src/Enums/HighlightColor.vala b/src/Enums/HighlightColor.vala index ccfc04a..1e1d6ca 100644 --- a/src/Enums/HighlightColor.vala +++ b/src/Enums/HighlightColor.vala @@ -3,6 +3,9 @@ * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) */ +// I like aligning the equals +//vala-lint=skip-file + /** * Used in the custom TextView, alternates background color between sentences. */ diff --git a/src/Enums/StatusCode.vala b/src/Enums/StatusCode.vala index 0a2d4c5..c02f968 100644 --- a/src/Enums/StatusCode.vala +++ b/src/Enums/StatusCode.vala @@ -3,15 +3,139 @@ * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) */ +// I like aligning the equals +//vala-lint=skip-file + /** * libsoup.Status does not have all error codes, so we extend handling in ErrorView/ErrorBonuxBox with these custom ones. * Custom codes we can use safely for at least 0 to 10. * Specific return codes to DeepL, we use their Int representation. */ public enum Inscriptions.StatusCode { - NO_KEY = 0, - NO_INTERNET = 1, - QUOTA = 456, - TOO_MANY_REQUESTS = 429, - SSL_HANDSHAKE_ERROR = 525; -} \ No newline at end of file + + // Custom + NO_KEY = 0, + NO_INTERNET = 1, + EDIT_KEY = 2, + UNKNOWN = 3, + + // 200 + OK = Soup.Status.OK, + + // 400 + BAD_REQUEST = Soup.Status.BAD_REQUEST, + FORBIDDEN = Soup.Status.FORBIDDEN, + REQUEST_TIMEOUT = Soup.Status.REQUEST_TIMEOUT, + TOO_MANY_REQUESTS = 429, + QUOTA = 456, + + // 500 + INTERNAL_SERVER_ERROR = Soup.Status.INTERNAL_SERVER_ERROR, + GATEWAY_TIMEOUT = Soup.Status.GATEWAY_TIMEOUT, + SSL_HANDSHAKE_ERROR = 525 + ; + + /** + * Utility function to make use of the various error codes, and return comprehensive explanations. + * It is here better to take the status than let the backend tell us, so we conserve the code for unknown errors. + * Yes, some refers to Soup.Status making the declaration above redundant, but i feel it may be cleaner to keep them in case we need a more native approach. + */ + public static void status_to_details (uint status, + out string explanation_title, out string explanation_text, out string icon_name, out bool report_link) { + + // Set some defaults + icon_name = "dialog-error"; + report_link = false; + + switch (status) { + //Custom status codes feel super evil + //TRANSLATORS: The following texts show up respectively, as a title, and error message, when translating has gone wrong. This needs to be as little technical as possible + case StatusCode.NO_KEY: + explanation_title = _("Hello, World!"); + explanation_text = _("You need a DeepL API key to translate text\nIt can be either DeepL Free or Pro"); + icon_name = "dialog-password"; + return; + + case StatusCode.NO_INTERNET: + explanation_title = _("No Internet"); + icon_name = "network-offline-symbolic"; + + if (Environment.get_variable ("XDG_CURRENT_DESKTOP") == "Pantheon") { + ///TRANSLATORS: This is twice the same text, but the first one has links for elementary OS + explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it").printf (Granite.SettingsUri.NETWORK, Granite.SettingsUri.PERMISSIONS); + } else { + explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it"); + } + return; + + case StatusCode.EDIT_KEY: + explanation_title = _("Edit API Key"); + explanation_text = _("Yo"); + icon_name = "dialog-password"; + return; + + + case Soup.Status.OK: + explanation_title = _("Everything works great :)"); + explanation_text = _("If you see this and are not me, then it means i forgor to disable this error"); + icon_name = "process-completed"; + report_link = true; + return; + + case Soup.Status.BAD_REQUEST: + explanation_title = _("Bad request"); + explanation_text = _("The app sent a wrong translation request to DeepL\nPlease report this to the app's developer with as much details as you can"); + icon_name = "dialog-warning"; + return; + + case Soup.Status.FORBIDDEN: + explanation_title = _("Forbidden"); + explanation_text = _("Your API key is invalid. Make sure it is the correct one!"); + icon_name = "dialog-error"; + return; + + case StatusCode.TOO_MANY_REQUESTS: + explanation_title = _("Too many requests"); + explanation_text = _("Please wait before retrying. This error should not be possible to happen for this app..."); + icon_name = "dialog-warning"; + return; + + case StatusCode.QUOTA: + explanation_title = _("Your monthly quota has been exceeded"); + explanation_text = _("If you are a Pro API user, this corresponds to your Cost Control limit"); + icon_name = "dialog-warning"; + return; + + case Soup.Status.INTERNAL_SERVER_ERROR: + explanation_title = _("Internal server error"); + explanation_text = _("Retry in a minute? If you see this several times, check online if there is a DeepL service interruption"); + icon_name = "dialog-information"; + return; + + case StatusCode.SSL_HANDSHAKE_ERROR: + explanation_title = _("SSL Handshake error"); + explanation_text = _("This is an issue DeepL is aware of and this app can do nothing about...\nIf you have the know-show, going through a simple authenticated proxy may work"); + icon_name = "network-error"; + return; + + case Soup.Status.REQUEST_TIMEOUT: + explanation_title = _("Request timeout"); + explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); + icon_name = "network-error"; + return; + + case Soup.Status.GATEWAY_TIMEOUT: + explanation_title = _("Gateway timeout"); + explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); + icon_name = "network-error"; + return; + + default: + explanation_title = _("Unknown error"); + explanation_text = _("Status code %s, please report this to this app's developer").printf(status.to_string ()); + icon_name = "dialog-question"; + report_link = true; + return; + } + } +} diff --git a/src/Enums/ZoomLevel.vala b/src/Enums/ZoomLevel.vala new file mode 100644 index 0000000..b277a6b --- /dev/null +++ b/src/Enums/ZoomLevel.vala @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/*************************************************/ +/** +* A register of all possible zoom values we have +*/ +public enum Inscriptions.ZoomLevel { + ANTSIZED, + MUCHSMALLER, + SMALLER, + SMALL, + NORMAL, + BIG, + BIGGER, + MUCHBIGGER, + MUCHMUCHBIGGER, + HUGE, + SUPERHUGE, + MEGAHUGE, + ULTRAHUGE, + MASSIVE, + URPARENT; + + /*************************************************/ + /** + * Returns an Int representation we can use to display and store the value + */ + public int to_int () { + switch (this) { + case ANTSIZED: return 20; + case MUCHSMALLER: return 40; + case SMALLER: return 60; + case SMALL: return 80; + case NORMAL: return 100; + case BIG: return 120; + case BIGGER: return 140; + case MUCHBIGGER: return 160; + case MUCHMUCHBIGGER: return 180; + case HUGE: return 200; + case SUPERHUGE: return 220; + case MEGAHUGE: return 240; + case ULTRAHUGE: return 260; + case MASSIVE: return 280; + case URPARENT: return 300; + default: return 100; + } + } + + /*************************************************/ + /** + * CSS name is s + size. CSS classes cannot start name with number + */ + public string to_css_class () { + return "s" + this.to_int ().to_string (); + } + + /*************************************************/ + /** + * We cannot save Enums in JSON, so this recovers the enum from stored int + */ + public static ZoomLevel from_int (int wtf_is_this) { + switch (wtf_is_this) { + case 20: return ANTSIZED; + case 40: return MUCHSMALLER; + case 60: return SMALLER; + case 80: return SMALL; + case 100: return NORMAL; + case 120: return BIG; + case 140: return BIGGER; + case 160: return MUCHBIGGER; + case 180: return MUCHMUCHBIGGER; + case 200: return HUGE; + case 220: return SUPERHUGE; + case 240: return MEGAHUGE; + case 260: return ULTRAHUGE; + case 280: return MASSIVE; + case 300: return URPARENT; + default: return NORMAL; + } + } +} diff --git a/src/Enums/ZoomType.vala b/src/Enums/ZoomType.vala new file mode 100644 index 0000000..bbdc38f --- /dev/null +++ b/src/Enums/ZoomType.vala @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + + /*************************************************/ +/** +* Used in a signal to tell windows in which way to change zoom +*/ + public enum Inscriptions.ZoomType { + ZOOM_OUT, + DEFAULT_ZOOM, + ZOOM_IN, + NONE; + + public static ZoomType from_delta (double delta) { + + if (delta == 0) {return NONE;} + + if (delta > 0) + { + return ZOOM_OUT; + + } else { + return ZOOM_IN; + } + } +} \ No newline at end of file diff --git a/src/Objects/DDModel.vala b/src/Objects/DDModel.vala index 3024767..2c91e68 100644 --- a/src/Objects/DDModel.vala +++ b/src/Objects/DDModel.vala @@ -6,29 +6,78 @@ /** * DDModel, to manage lists of Lang objects (Languages), notably for the dropdown of Pane - * Thank you stronnag! + * In human, this is to have a custom dropdown for language selection */ public class Inscriptions.DDModel : Object { public GLib.ListStore model {get; set;} - public Gtk.SignalListItemFactory factory {get; set;} + public Gtk.SignalListItemFactory factory_header {get; set;} + public Gtk.SignalListItemFactory factory_list {get; set;} - public DDModel() { + // Signal emitted by factory_header.bind when a language is selected, which factory_list.bind listens to + public signal void selection_changed (string language_code_selected); + + public DDModel () { + // The Langs will populate this thing model = new GLib.ListStore(typeof(Lang)); - factory = new Gtk.SignalListItemFactory(); - factory.setup.connect ((f,o) => { - Gtk.ListItem list_item = (Gtk.ListItem)o; - var label=new Gtk.Label(""); - list_item.set_child(label); - }); - factory.bind.connect ((f,o) => { - Gtk.ListItem list_item = (Gtk.ListItem)o; - var language = list_item.get_item () as Lang; - var label = list_item.get_child() as Gtk.Label; - label.set_text(language.name); - }); - } - - public void model_append(Lang l) { + + factory_header = new Gtk.SignalListItemFactory(); + factory_list = new Gtk.SignalListItemFactory(); + + // This one does simple labels for each language, to be shown by the dropdown + factory_header.setup.connect (on_factory_header_setup); + factory_header.bind.connect (on_factory_header_bind); + + // This one generates a custom widget for each element in the popup list + factory_list.setup.connect (on_factory_list_setup); + factory_list.bind.connect (on_factory_list_bind); + } + + // ---------------------------------------- + /* DROPDOWN VISIBLE LABEL */ + private void on_factory_header_setup (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + list_item.child = new Gtk.Label (""); + list_item.focusable = true; + } + + private void on_factory_header_bind (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var item_language = list_item.get_item () as Lang; + var item = list_item.get_child () as Gtk.Label; + item.label = item_language.name; + + // Tell everyone language changed + selection_changed (item_language.code); + //print ("switched to: %s %s\n".printf (item_language.name, item_language.code)); + } + + // ---------------------------------------- + /* DROPDOWN POPUP LIST */ + private void on_factory_list_setup (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var list_item_child = new Inscriptions.LanguageItem ("", ""); + + list_item.child = list_item_child; + list_item.focusable = true; + } + + private void on_factory_list_bind (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var item_language = list_item.get_item () as Lang; + + var list_item_child = list_item.get_child() as Inscriptions.LanguageItem; + list_item_child.language_label = item_language.name; + list_item_child.language_code = item_language.code; + + // Listen to language change, let every item sort its shit + selection_changed.connect (list_item_child.on_position_changed); + //print ("binding: %s\n".printf (item_language.name)); + } + + + // ---------------------------------------- + /* LIST MANAGEMENT */ + public void model_append (Lang l) { model.append (l); } diff --git a/src/Objects/Lang.vala b/src/Objects/Lang.vala index 36bcbfa..c22c68c 100644 --- a/src/Objects/Lang.vala +++ b/src/Objects/Lang.vala @@ -12,15 +12,16 @@ public class Inscriptions.Lang : Object { public string code {get; construct;} public string name {get; construct;} public string both {get; construct;} + public bool enabled {get; set; default = false;} public Lang (string code, string name) { - Object( code: code, + Object (code: code, name: name); } // "Both" serves to evaluate both name and code in a single expression construct { - both = name + "|" + code; + both = "%s|%s".printf(name, code); } public bool efunc(Lang a, Lang b) { diff --git a/src/Services/ZoomController.vala b/src/Services/ZoomController.vala new file mode 100644 index 0000000..2dd2fe7 --- /dev/null +++ b/src/Services/ZoomController.vala @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/*************************************************/ +/** +* Responsible to apply zoom appropriately to a window. +* Mainly, this abstracts zoom into an int and swap CSS classes +* As a treat it includes also the plumbing for ctrl+scroll zooming +*/ +public class Inscriptions.ZoomController : Object { + + private static bool is_control_key_pressed = false; + private weak Gtk.Widget widget; + + const int ZOOM_MAX = 300; + const int DEFAULT_ZOOM = 100; + const int ZOOM_MIN = 20; + + + // Avoid setting this unless it is to restore a specific value, do_set_zoom does not check input + private int _old_zoom = DEFAULT_ZOOM; + public int zoom { + get {return _old_zoom;} + set {do_set_zoom (value);} + } + + public ZoomController (Gtk.Widget widget) { + this.widget = widget; + do_set_zoom (DEFAULT_ZOOM); + } + + /** + * Handler. Wraps a zoom enum into the correct function- + */ + public void zoom_changed (ZoomType zoomtype) { + print ("Zoom changed!"); + switch (zoomtype) { + case ZoomType.ZOOM_IN: zoom_in (); return; // vala-lint=double-spaces + case ZoomType.DEFAULT_ZOOM: zoom_default (); return; // vala-lint=double-spaces + case ZoomType.ZOOM_OUT: zoom_out (); return; // vala-lint=double-spaces + default: return; // vala-lint=double-spaces + } + } + + /** + * Wrapper to check an increase doesnt go above limit + */ + public void zoom_in () { + if ((_old_zoom + 20) <= ZOOM_MAX) { + zoom = _old_zoom + 20; + } else { + Gdk.Display.get_default ().beep (); + } + } + + public void zoom_default () { + if (_old_zoom != DEFAULT_ZOOM ) { + zoom = DEFAULT_ZOOM; + } else { + Gdk.Display.get_default ().beep (); + } + } + + /** + * Wrapper to check an increase doesnt go below limit + */ + public void zoom_out () { + if ((_old_zoom - 20) >= ZOOM_MIN) { + zoom = _old_zoom - 20; + } else { + Gdk.Display.get_default ().beep (); + } + } + + /** + * Switch zoom classes, then reflect in the UI and tell the application + */ + private void do_set_zoom (int new_zoom) { + print ("Setting zoom: " + zoom.to_string ()); + + // Switches the classes that control font size + widget.remove_css_class (ZoomLevel.from_int ( _old_zoom).to_css_class ()); + _old_zoom = new_zoom; + widget.add_css_class (ZoomLevel.from_int ( new_zoom).to_css_class ()); + } + + public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + print ("Press!"); + is_control_key_pressed = true; + } + + return Gdk.EVENT_PROPAGATE; + } + + public void on_key_release_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + print ("Release!"); + is_control_key_pressed = false; + } + } + + public bool on_scroll (double dx, double dy) { + //print ("Scroll + Ctrl!"); + + if (!is_control_key_pressed) { + return Gdk.EVENT_PROPAGATE; + } + + zoom_changed (ZoomType.from_delta (dy)); + //print ("Go! Zoooommmmm"); + + return Gdk.EVENT_PROPAGATE; + } +} diff --git a/src/Views/ErrorView.vala b/src/Views/ErrorView.vala index 302ca13..1e734da 100644 --- a/src/Views/ErrorView.vala +++ b/src/Views/ErrorView.vala @@ -11,10 +11,12 @@ public class Inscriptions.ErrorView : Granite.Bin { const uint WAIT_BEFORE_MAIN = 1500; //In milliseconds + ErrorBonusBox bonusbox; + public uint status { get; construct; } public string message { get; construct; } - - string icon_name = "dialog-error"; + + string icon_name; string explanation_title; string explanation_text; bool report_link; @@ -38,7 +40,8 @@ public class Inscriptions.ErrorView : Granite.Bin { margin_bottom = MARGIN_MENU_BIGGER, }; - status_to_message (status); + Inscriptions.StatusCode.status_to_details (status, + out explanation_title, out explanation_text, out icon_name, out report_link); var title = new Granite.Placeholder (explanation_title) { description = explanation_text, @@ -49,7 +52,8 @@ public class Inscriptions.ErrorView : Granite.Bin { box.append (title); // WEIRD: We get errors about TRUE being out of range for a gboolean and the value defaulting if we leave a default - box.append (new ErrorBonusBox (status, report_link)); + bonusbox = new ErrorBonusBox (status, report_link); + box.append (bonusbox); var retry_button = new Inscriptions.RetryButton () { halign = Gtk.Align.END @@ -80,7 +84,7 @@ public class Inscriptions.ErrorView : Granite.Bin { margin_top = 12 }; - if (status != StatusCode.NO_KEY) { + if (status != StatusCode.NO_KEY || status != StatusCode.EDIT_KEY) { box.append (expander); } @@ -89,96 +93,10 @@ public class Inscriptions.ErrorView : Granite.Bin { }; child = handle; - - } - - private void status_to_message (uint status) { - switch (status) { - //Custom status codes feel super evil - //TRANSLATORS: The following texts show up respectively, as a title, and error message, when translating has gone wrong. This needs to be as little technical as possible - case StatusCode.NO_KEY: - explanation_title = _("Hello, World!"); - explanation_text = _("You need a DeepL API key to translate text\nIt can be either DeepL Free or Pro"); - icon_name = "dialog-password"; - return; - - case StatusCode.NO_INTERNET: - explanation_title = _("No Internet"); - icon_name = "network-offline-symbolic"; - - if (Environment.get_variable ("XDG_CURRENT_DESKTOP") == "Pantheon") { - ///TRANSLATORS: This is twice the same text, but the first one has links for elementary OS - explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it").printf (Granite.SettingsUri.NETWORK, Granite.SettingsUri.PERMISSIONS); - } else { - explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it"); - } - - return; - - case Soup.Status.OK: - explanation_title = _("Everything works great :)"); - explanation_text = _("If you see this and are not me, then it means i forgor to disable this error"); - icon_name = "process-completed"; - return; - - case Soup.Status.BAD_REQUEST: - explanation_title = _("Bad request"); - explanation_text = _("The app sent a wrong translation request to DeepL\nPlease report this to the app's developer with as much details as you can"); - icon_name = "dialog-warning"; - return; - - case Soup.Status.FORBIDDEN: - explanation_title = _("Forbidden"); - explanation_text = _("Your API key is invalid. Make sure it is the correct one!"); - icon_name = "dialog-error"; - return; - - case StatusCode.TOO_MANY_REQUESTS: - explanation_title = _("Too many requests"); - explanation_text = _("Please wait before retrying. This error should not be possible to happen for this app..."); - icon_name = "dialog-warning"; - return; - - case StatusCode.QUOTA: - explanation_title = _("Your monthly quota has been exceeded"); - explanation_text = _("If you are a Pro API user, this corresponds to your Cost Control limit"); - icon_name = "dialog-warning"; - return; - - case Soup.Status.INTERNAL_SERVER_ERROR: - explanation_title = _("Internal server error"); - explanation_text = _("Retry in a minute? If you see this several times, check online if there is a DeepL service interruption"); - icon_name = "dialog-information"; - return; - - case StatusCode.SSL_HANDSHAKE_ERROR: - explanation_title = _("SSL Handshake error"); - explanation_text = _("This is an issue DeepL is aware of and this app can do nothing about...\nIf you have the know-show, going through a simple authenticated proxy may work"); - icon_name = "network-error"; - return; - - case Soup.Status.REQUEST_TIMEOUT: - explanation_title = _("Request timeout"); - explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); - icon_name = "network-error"; - return; - - case Soup.Status.GATEWAY_TIMEOUT: - explanation_title = _("Gateway timeout"); - explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); - icon_name = "network-error"; - return; - - default: - explanation_title = _("Unknown error"); - explanation_text = _("Status code %s, please report this to this app's developer").printf(status.to_string ()); - icon_name = "dialog-question"; - report_link = true; - return; - } } private void on_validated () { + bonusbox.usage_revealer.reveal_child = true; Timeout.add_once (WAIT_BEFORE_MAIN, () => { return_to_main (); }); diff --git a/src/Views/TranslationView.vala b/src/Views/TranslationView.vala index a2a7084..13f5d7a 100644 --- a/src/Views/TranslationView.vala +++ b/src/Views/TranslationView.vala @@ -216,7 +216,7 @@ public class Inscriptions.TranslationView : Gtk.Box { source_pane.clear (); target_pane.clear (); target_pane.show_placeholder (); - source_pane.message (_("Cleared")); + source_pane.message (_("Cleared"), true); } public void action_load_text () { diff --git a/src/Widgets/ErrorBonusBox.vala b/src/Widgets/ErrorBonusBox.vala index 765bc62..5f6ca0c 100644 --- a/src/Widgets/ErrorBonusBox.vala +++ b/src/Widgets/ErrorBonusBox.vala @@ -14,6 +14,8 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { public uint status { get; construct; } public bool if_report { get; construct; } + public Gtk.Revealer usage_revealer; + public ErrorBonusBox (uint status, bool if_report) { Object (status: status, if_report: if_report); @@ -26,7 +28,9 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { margin_bottom = MARGIN_MENU_STANDARD; // In the event the API is the issue, ask user - if (status == Soup.Status.FORBIDDEN || status == StatusCode.NO_KEY) { + StatusCode[] api_edit_list = {StatusCode.NO_KEY, StatusCode.FORBIDDEN, StatusCode.EDIT_KEY}; + + if (status in api_edit_list) { var api_entry = new Inscriptions.ApiEntry (); @@ -35,7 +39,16 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { halign = Gtk.Align.START }; - if (status == StatusCode.NO_KEY) { + var api_level = new Inscriptions.ApiLevel (); + usage_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + transition_duration = 500, + child = api_level, + reveal_child = (status != StatusCode.NO_KEY) + }; + + + if (status == StatusCode.NO_KEY || status == StatusCode.FORBIDDEN) { var explanation = new Gtk.Label (_("An API Key is like a password given by DeepL\nIt allows you to access services from applications such as this one\nIt looks like this: fr5617a-4875-4763-9119-564tjdvg89:fx")) { wrap_mode = Pango.WrapMode.WORD_CHAR, halign = Gtk.Align.START @@ -47,6 +60,11 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { append (api_entry); append (link); + //append (usage_revealer); + + + + }; diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 7159fc0..d995917 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -65,7 +65,10 @@ public class Inscriptions.HeaderBar : Granite.Bin { app.set_accels_for_action (ACTION_PREFIX + ACTION_MENU, {"m"}); app.set_accels_for_action (ACTION_PREFIX + ACTION_TOGGLE_MESSAGES, {"m"}); - title_label = new Gtk.Label (_("Inscriptions")); + title_label = new Gtk.Label (APP_NAME); +#if DEVEL + title_label.label += _(" (Devel)"); +#endif title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); title_switcher = new Gtk.StackSwitcher () { @@ -116,7 +119,7 @@ public class Inscriptions.HeaderBar : Granite.Bin { var toolbar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5); toolbar.append (switchlang_button); - toolbar.append (toggle_highlight); + //toolbar.append (toggle_highlight); toolbar_revealer = new Gtk.Revealer () { child = toolbar, diff --git a/src/Widgets/LanguageItem.vala b/src/Widgets/LanguageItem.vala new file mode 100644 index 0000000..b99c74c --- /dev/null +++ b/src/Widgets/LanguageItem.vala @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * Used by DDModel to display Lang items in a Pane Dropdown + */ +public class Inscriptions.LanguageItem : Gtk.Box { + + public string language_label {get; set;} + public string language_code {get; set;} + + Gtk.Label label_widget; + Gtk.Image selected_emblem; + + public LanguageItem (string language_label, string language_code) { + Object ( + language_label: language_label, + language_code: language_code, + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 0, + halign: Gtk.Align.FILL, + hexpand: true + ); + } + + construct { + selected_emblem = new Gtk.Image.from_icon_name ("emblem-default-symbolic") { + visible = false, + halign = Gtk.Align.START + }; + selected_emblem.add_css_class (Granite.STYLE_CLASS_FLAT); + + label_widget = new Gtk.Label (language_label) { + halign = Gtk.Align.CENTER, + hexpand = true, + xalign = 0.5f + }; + + var overlay = new Gtk.Overlay () { + child = label_widget + }; + overlay.add_overlay (selected_emblem); + + append (overlay); + + bind_property ("language-label", + label_widget, "label", + GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE); + } + + public void on_position_changed (string language_code_selected) { + + if (language_code_selected == language_code) { + label_widget.add_css_class ("bold"); + selected_emblem.visible = true; + + } else { + label_widget.remove_css_class ("bold"); + selected_emblem.visible = false; + } + } +} diff --git a/src/Widgets/Panes/Pane.vala b/src/Widgets/Panes/Pane.vala index 05e7f45..b46348f 100644 --- a/src/Widgets/Panes/Pane.vala +++ b/src/Widgets/Panes/Pane.vala @@ -47,13 +47,14 @@ public class Inscriptions.Pane : Gtk.Box { /* ---------------- DROPDOWN ---------------- */ dropdown = new Gtk.DropDown (model.model, expression) { - factory = model.factory, + factory = model.factory_header, + list_factory = model.factory_list, enable_search = true, - search_match_mode= Gtk.StringFilterMatchMode.SUBSTRING + search_match_mode= Gtk.StringFilterMatchMode.SUBSTRING, + show_arrow = false }; dropdown.notify["selected-item"].connect(on_selected_language); - /* ---------------- VIEW ---------------- */ textview = new Inscriptions.TextView (); textview.set_wrap_mode (Gtk.WrapMode.WORD_CHAR); @@ -85,7 +86,6 @@ public class Inscriptions.Pane : Gtk.Box { child = actionbar }; - /* ---------------- STACK ---------------- */ main_view = new Gtk.Box (VERTICAL, 0); @@ -99,6 +99,10 @@ public class Inscriptions.Pane : Gtk.Box { append (dropdown); append (stack); + + toast.default_action.connect (() => { + textview.buffer.undo (); + }); } public void on_selected_language () { @@ -124,12 +128,31 @@ public class Inscriptions.Pane : Gtk.Box { return selected.name; } + // Respectful of Undo + public void replace_text (string new_text) { + + Gtk.TextIter start, end; + textview.buffer.get_bounds (out start, out end); + + textview.buffer.begin_user_action (); + this.textview.buffer.delete (ref start, ref end); + this.textview.buffer.insert (ref start, new_text, new_text.length); + textview.buffer.end_user_action (); + + textview.grab_focus (); + } + public void clear () { - this.textview.buffer.text = ""; + replace_text (""); } - public void message (string text) { + public void message (string text, bool? undo = false) { toast.title = text; + + if (undo) { + toast.set_default_action (_("Undo")); + } + toast.send_notification (); } } diff --git a/src/Widgets/Panes/SourcePane.vala b/src/Widgets/Panes/SourcePane.vala index c6a8fa4..7930be6 100644 --- a/src/Widgets/Panes/SourcePane.vala +++ b/src/Widgets/Panes/SourcePane.vala @@ -28,7 +28,7 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { var options_button = new Gtk.MenuButton () { child = options_button_box, tooltip_text = _("Change options for the translation"), - margin_end = 6 + margin_end = MARGIN_MENU_STANDARD }; options_button.add_css_class (Granite.STYLE_CLASS_FLAT); options_button.add_css_class ("flat_menu_button"); @@ -39,22 +39,23 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { actionbar.pack_start (options_button); - var clear = new Gtk.Button.from_icon_name ("edit-clear-all-symbolic") { + var clear_button = new Gtk.Button.from_icon_name ("edit-clear-all-symbolic") { action_name = TranslationView.ACTION_PREFIX + TranslationView.ACTION_CLEAR_TEXT, tooltip_markup = Granite.markup_accel_tooltip ( {"L"}, _("Clear text") ), - margin_start = MARGIN_MENU_HALF + margin_start = MARGIN_MENU_HALF, + sensitive = false }; - var paste = new Gtk.Button.from_icon_name ("edit-paste-symbolic") { + var paste_button = new Gtk.Button.from_icon_name ("edit-paste-symbolic") { tooltip_text = _("Paste from clipboard"), - margin_start = 3 + margin_start = MARGIN_MENU_HALF }; - actionbar.pack_end (clear); - actionbar.pack_end (paste); + actionbar.pack_end (clear_button); + actionbar.pack_end (paste_button); var open_button = new Gtk.Button.from_icon_name ("document-open-symbolic") { action_name = TranslationView.ACTION_PREFIX + TranslationView.ACTION_LOAD_TEXT, @@ -73,8 +74,12 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { GLib.SettingsBindFlags.DEFAULT ); - paste.clicked.connect (paste_from_clipboard); + paste_button.clicked.connect (paste_from_clipboard); language_changed.connect (on_language_changed); + + textview.buffer.changed.connect (() => { + clear_button.sensitive = (text != ""); + }); } private void on_language_changed (string code) { @@ -90,8 +95,8 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { try { var pasted_text = clipboard.read_text_async.end (res); - textview.buffer.text = pasted_text; - message (_("Pasted")); + replace_text (pasted_text); + message (_("Pasted"), true); } catch (Error e) { print ("Cannot access clipboard: " + e.message); @@ -100,7 +105,6 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { } public void action_load_text () { - var all_files_filter = new Gtk.FileFilter () { name = _("All files"), }; @@ -130,7 +134,8 @@ public class Inscriptions.SourcePane : Inscriptions.Pane { var content = ""; FileUtils.get_contents (file.get_path (), out content); - this.text = content; + replace_text (content); + message (_("Loaded from file"), true); } catch (Error err) { warning ("Failed to select file to open: %s", err.message); diff --git a/src/Widgets/Panes/TargetPane.vala b/src/Widgets/Panes/TargetPane.vala index 52228af..c1830ec 100644 --- a/src/Widgets/Panes/TargetPane.vala +++ b/src/Widgets/Panes/TargetPane.vala @@ -44,7 +44,7 @@ public class Inscriptions.TargetPane : Inscriptions.Pane { }; placeholder_box.append (placeholder); - placeholder_box.append (placeholder_switcher); + //placeholder_box.append (placeholder_switcher); placeholder_handle = new Gtk.WindowHandle () { child = placeholder_box @@ -71,7 +71,7 @@ public class Inscriptions.TargetPane : Inscriptions.Pane { tooltip_text = _("Switch between click to translate // translate %.2fs after typing has stopped").printf (DEBOUNCE_IN_S) }; - actionbar.pack_start (auto_switcher); + //actionbar.pack_start (auto_switcher); /* -------- TOOLBAR -------- */ var copy = new Gtk.Button.from_icon_name ("edit-copy-symbolic") { diff --git a/src/Widgets/PopoverWidgets/ApiLevel.vala b/src/Widgets/PopoverWidgets/ApiLevel.vala index df2e12c..f3e0911 100644 --- a/src/Widgets/PopoverWidgets/ApiLevel.vala +++ b/src/Widgets/PopoverWidgets/ApiLevel.vala @@ -10,8 +10,8 @@ public class Inscriptions.ApiLevel : Gtk.Box { Gtk.LevelBar api_usage; - Gtk.Spinner loading; - Gtk.Stack refresher; + Gtk.Spinner spinner; + Gtk.Button refresh_button; construct { orientation = Gtk.Orientation.VERTICAL; @@ -28,18 +28,34 @@ public class Inscriptions.ApiLevel : Gtk.Box { api_usage_label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); cb.start_widget = api_usage_label; - refresher = new Gtk.Stack (); + spinner = new Gtk.Spinner () { + spinning = false, + visible = false + }; - loading = new Gtk.Spinner (); - refresher.add_named (loading, "loading"); - var hint = new Gtk.Button.from_icon_name ("view-refresh") { + refresh_button = new Gtk.Button.from_icon_name ("view-refresh") { tooltip_text = _("Update API usage") }; - refresher.add_named (hint, "hint"); - refresher.visible_child_name = "hint"; - cb.end_widget = refresher; + var edit_key_button = new Gtk.Button.from_icon_name ("dialog-password") { + tooltip_text = _("Use a different API key of your choosing"), + }; + edit_key_button.add_css_class (Granite.STYLE_CLASS_FLAT); + edit_key_button.clicked.connect (() => { + Application.backend.answer_received (StatusCode.EDIT_KEY, _("Requested by user")); + }); + + + var minibox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, Inscriptions.SPACING_TOOLBAR_MINI) { + valign = Gtk.Align.CENTER + }; + + minibox.append (edit_key_button); + minibox.append (spinner); + minibox.append (refresh_button); + + cb.end_widget = minibox; append (cb); @@ -48,10 +64,18 @@ public class Inscriptions.ApiLevel : Gtk.Box { api_usage.min_value = 0; append (api_usage); + spinner.bind_property ("spinning", + refresh_button, "visible", + GLib.BindingFlags.INVERT_BOOLEAN | GLib.BindingFlags.SYNC_CREATE); + + spinner.bind_property ("spinning", + spinner, "visible", + GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE); + Application.settings.bind ("current-usage", api_usage, "value", SettingsBindFlags.DEFAULT); Application.settings.bind ("max-usage", api_usage, "max-value", SettingsBindFlags.DEFAULT); - hint.clicked.connect (on_refresh); + refresh_button.clicked.connect (on_refresh); Application.backend.answer_received.connect (updated_usage); Application.backend.usage_retrieved.connect (updated_usage); updated_usage (); @@ -81,15 +105,11 @@ public class Inscriptions.ApiLevel : Gtk.Box { Application.settings.get_int ("current-usage").to_string (), Application.settings.get_int ("max-usage").to_string ()); - if (refresher.visible_child_name == "loading") { - refresher.visible_child_name = "hint"; - loading.spinning = false; - } + spinner.spinning = false; } private void on_refresh () { - loading.spinning = true; - refresher.visible_child_name = "loading"; + spinner.spinning = true; Application.backend.check_usage (); } } diff --git a/src/Widgets/Popovers/SettingsPopover.vala b/src/Widgets/Popovers/SettingsPopover.vala index b978458..7d9989b 100644 --- a/src/Widgets/Popovers/SettingsPopover.vala +++ b/src/Widgets/Popovers/SettingsPopover.vala @@ -19,48 +19,18 @@ public class Inscriptions.SettingsPopover : Gtk.Popover { margin_bottom = MARGIN_MENU_STANDARD }; - //TRANSLATORS: The two following texts are for a switch button that does not show up in the UI - //The functionality is disabled. You can safely ignore this for the time being - var auto_switch = new Granite.SwitchModelButton (_("Translate automatically")) { - description = _("The translation will start %.2f seconds after typing has stopped".printf (DEBOUNCE_IN_S)), - hexpand = true, - margin_top = MARGIN_MENU_HALF - }; - /* -------------------- SEPARATOR -------------------- */ - var cb = new Gtk.CenterBox () { - margin_end = MARGIN_MENU_BIG - }; - var api_label = new Gtk.Label (_("DeepL API Key")) { - halign = Gtk.Align.START, - margin_start = MARGIN_MENU_BIG, - margin_top = MARGIN_MENU_HALF - }; - api_label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); - cb.start_widget = api_label; - - var hint = new Gtk.Button.from_icon_name ("help-contents") { - tooltip_text = _("You can get an API key here") - }; - cb.end_widget = hint; - - api_entry = new Inscriptions.ApiEntry () { - margin_start = MARGIN_MENU_BIG, - margin_end = MARGIN_MENU_BIG + var auto_switch = new Granite.SwitchModelButton (_("Translate automatically")) { + description = _("The translation will start %.2f seconds after typing has stopped".printf (DEBOUNCE_IN_S)), + hexpand = true }; - var api_level = new Inscriptions.ApiLevel () { - margin_start = MARGIN_MENU_BIG, - margin_end = MARGIN_MENU_BIG, - margin_top = MARGIN_MENU_HALF + var highlight_switch = new Granite.SwitchModelButton (_("Highlight source and target sentences")) { + description = _("Each line will be highlighted a different color to help compare both texts (Ctrl+H)"), + hexpand = true }; - usage_revealer = new Gtk.Revealer () { - transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, - transition_duration = 500, - child = api_level - }; /* -------------------- SEPARATOR -------------------- */ @@ -71,37 +41,31 @@ public class Inscriptions.SettingsPopover : Gtk.Popover { margin_start = MARGIN_MENU_BIG }; + var api_level = new ApiLevel () { + margin_top = MARGIN_MENU_HALF, + margin_start = MARGIN_MENU_BIG, + margin_end = MARGIN_MENU_BIG + }; + box.append (new OrientationBox ()); - //box.append (auto_switch); box.append (new Gtk.Separator (HORIZONTAL)); - box.append (cb); - box.append (api_entry); - box.append (usage_revealer); + box.append (auto_switch); + box.append (highlight_switch); + //box.append (edit_key_button); + box.append (api_level); box.append (new Gtk.Separator (HORIZONTAL)); box.append (support_button); child = box; /* -------------------- CONNECTS AND BINDS -------------------- */ - hint.clicked.connect (open_webpage); - api_entry.api_entry.changed.connect (relevant_levelbar); - relevant_levelbar (); - Application.settings.bind ("auto-translate", + Application.settings.bind (KEY_AUTO_TRANSLATE, auto_switch, "active", SettingsBindFlags.DEFAULT); - } - - private void relevant_levelbar () { - usage_revealer.reveal_child = (api_entry.api_entry.text != ""); - } - - private void open_webpage () { - try { - AppInfo.launch_default_for_uri (LINK, null); - } catch (Error e) { - warning ("%s\n", e.message); - } + Application.settings.bind (KEY_HIGHLIGHT, + highlight_switch, "active", + SettingsBindFlags.DEFAULT); } } diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index ffc7fbe..11b1dde 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -7,7 +7,7 @@ * A base object that is then subclassed into a SourcePane and a TargetPane. * It takes a DDModel to fill the dropdown with languages */ -public class Inscriptions.TextView : Gtk.TextView { +public class Inscriptions.TextView : Inscriptions.ZoomableTextView { static Gtk.Settings gtk_settings = Gtk.Settings.get_default (); HighlightColor[] all_colors = HighlightColor.all (); diff --git a/src/Widgets/ZoomableTextView.vala b/src/Widgets/ZoomableTextView.vala new file mode 100644 index 0000000..224cd4d --- /dev/null +++ b/src/Widgets/ZoomableTextView.vala @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * A base object that is then subclassed into a SourcePane and a TargetPane. + * It takes a DDModel to fill the dropdown with languages + */ +public class Inscriptions.ZoomableTextView : Gtk.TextView { + private ZoomController zoom_controller; + + construct { + zoom_controller = new ZoomController ((Gtk.Widget)this); + + var keypress_controller = new Gtk.EventControllerKey (); + var scroll_controller = new Gtk.EventControllerScroll (VERTICAL) { + propagation_phase = Gtk.PropagationPhase.CAPTURE + }; + + add_controller (keypress_controller); + add_controller (scroll_controller); + + // We need this for Ctr + Scroll. We delegate everything to zoomcontroller + keypress_controller.key_pressed.connect (zoom_controller.on_key_press_event); + keypress_controller.key_released.connect (zoom_controller.on_key_release_event); + scroll_controller.scroll.connect (zoom_controller.on_scroll); + } +} diff --git a/src/Windows/MainWindow.vala b/src/Windows/MainWindow.vala index 56d9659..093f762 100644 --- a/src/Windows/MainWindow.vala +++ b/src/Windows/MainWindow.vala @@ -25,6 +25,9 @@ public class Inscriptions.MainWindow : Gtk.ApplicationWindow { default_width = Application.settings.get_int (KEY_WINDOW_WIDTH); maximized = Application.settings.get_boolean (KEY_WINDOW_MAXIMIZED); +#if DEVEL + add_css_class ("devel"); +#endif /* ---------------- HEADERBAR ---------------- */ diff --git a/src/meson.build b/src/meson.build index 8d8ba29..bb2893f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -19,6 +19,7 @@ sources = files ( 'Widgets' / 'Panes' / 'SourcePane.vala', 'Widgets' / 'Panes' / 'TargetPane.vala', + 'Widgets' / 'LanguageItem.vala', 'Widgets' / 'LogToolbar.vala', 'Widgets' / 'ErrorBonusBox.vala', 'Widgets' / 'TextView.vala', @@ -33,6 +34,12 @@ sources = files ( 'Widgets' / 'PopoverWidgets' / 'ApiEntry.vala', 'Widgets' / 'PopoverWidgets' / 'ApiLevel.vala', + 'Enums' / 'ZoomType.vala', + 'Enums' / 'ZoomLevel.vala', + 'Services' / 'ZoomController.vala', + 'Widgets' / 'ZoomableTextView.vala', + + 'Views' / 'TranslationView.vala', 'Views' / 'ErrorView.vala', 'Views' / 'LogView.vala', @@ -43,7 +50,7 @@ sources = files ( # Create a new executable, list the files we want to compile, and install executable( - meson.project_name (), + app_id, config_file, gresource, sources,