arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 31

Developer Documentation

Loading...

Writing Apps

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

APIs

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

AppCenter

Loading...

Loading...

Loading...

Loading...

Loading...

Developer Docs

Creating and distributing apps for elementary OS

hashtag
Do I need to know Vala?

The examples in this documentation use Vala, but the libraries and concepts used to build apps for elementary OS also work with a number of popular languages like C, Python, Go, Rust, etc. We don't assume extensive expertise in Vala in this guide, but basic programming knowledge and experience with object-oriented programming languages will be valuable.

If you're not familiar with Vala and want to learn, there are great resources available on the Vala websitearrow-up-right

hashtag
Does this guide cover design principles?

Design is covered in the . We reference the HIG throughout this guide and it's important you grasp the concepts proposed there, but this guide is focused primarily on code.

hashtag
Do I need to read this guide in order?

We recommend to at least skim "" first, but sections are generally written to be standalone tutorials and will reference other parts of the guide where necessary.

hashtag
Where I can get help?

We use GitHub discussions for Q&A as well as a place to submit ideas or show off what you're working on!

hashtag
Write Apps

hashtag
Publish in AppCenter

Human Interface Guidelines (HIG)arrow-up-right
The Basic Setup
Join the communityarrow-up-right

Metadata

List your app in AppCenter and make it accessible from the Applications menu and Dock.

Every app comes with two metadata files that we can generate using an online tool. Open AppStream Metainfo Creatorarrow-up-right and fill out the form with your app's info:

circle-info

When you get the section titled "Launchables", make sure to select "Generate a .desktop file for me".

Don't worry about generating Meson snippets, as we'll cover that in the next section. After you select "Generate", you should have two resulting files that you can copy.

hashtag
MetaInfo

First is a MetaInfo file. This file contains all the information needed to list your app in AppCenter. It should look something like this:

In your project's root, create a new folder called "data", and save your MetaInfo to a new file called "hello-again.metainfo.xml".

circle-info

Below are some of the most important fields for publishing in AppCenter, but there are even more that you can read about

hashtag
Screenshots

For the purposes of this tutorial, screenshots are optional, but they are required for publishing in AppCenter. Screenshots should only show your app on a transparent background and not contain any additional text or illustrations. You can use the caption tag to provide translatable and accessible descriptions of your screenshots.

circle-info

You can use the built-in Screenshot app on elementary OS with the "grab the current window" option or secondary-click on your app's title area and select "Take Screenshot" to get transparent window-only screenshots of your app

hashtag
Content Warnings

We use the standard to describe sensitive content that may be present in your app so that people using it can be informed and actively consent to seeing that content. OARS data is required and can be generated by taking :

hashtag
Branding

You can also specify a brand color for your app by adding the branding tag inside the component tag. Colors must be in hexadecimal, starting with #. The background will automatically be given a slight gradient in your app's banner.

hashtag
Monetization

If you want to monetize your app, you will need to add two keys inside a custom tag inside the component tag. Suggested prices should be in whole USD. You also must include your app's AppCenter Stripe key. This is a unique public key for each app and is not the same as your Stripe account's regular public key. You can connect your app to Stripe and receive a new key on the .

circle-info

Remember that AppCenter is a pay-what-you-want store. A suggested price is not a price floor. Users will still be able to choose any price they like, including 0.

hashtag
Releases

Your app must have a release tag for every version you wish to publish in AppCenter, including the initial release.

circle-info

For more details on available features and advice on writing engaging release notes see .

hashtag
Desktop Entry

This file contains all the information needed to display your app in the Applications Menu and in the Dock. The one generated from AppStream Metainfo Creator looks something like this:

Copy the contents of your Desktop Entry and save it to the data folder you created earlier. Name this new file "hello-again.desktop".

circle-info

For more info about crafting .desktop files, check out .

hashtag
Mark Your Progress

Each time we add a new file or make a significant change it's a good idea to commit a new revision and push to GitHub. Keep in mind that this acts as a backup system as well; when we push our work to GitHub, we know it's safe and we can always revert to a known good revision if we mess up later.

Add all of these files to git and commit a revision:

Now that we've got all these swanky files laying around, we need a way to tell the computer what to do with them. Ready for the next chapter? Let's do this!

Our First App

Launch-able, Build-able, and Shareable

In the previous chapter, we created a "Hello World!" app to show off our Vala and Gtk skills. But what if we wanted to share our new app with a friend? They'd have to know which packages to include with the valac command we used to build our app, and after they'd built it they'd have to run it from the build directory like we did. Clearly, we need to do some more stuff to make our app fit for people to use, to make it a real app.

hashtag
Hello (Again) World!

To create our first real app, we're going to re-use what we learned in the last chapter and build on that example.

  1. Create a new project folder, including a new "src" folder and an Application.vala file

  2. Create a Gtk.Application class with a Gtk.ApplicationWindow in the activate () function.

The results should look like this:

Build "Application.vala" just to make sure it all works. If something goes wrong here, feel free to refer back to and remember to check your terminal output for any hints.

Initialize a git branch, add your files to the project, and write a commit message using what you learned in the last chapter. Lastly, make sure you've created a new repository for your project on GitHub push your first revision with git:

Everything working as expected? Good. Now, let's get our app ready for other people to use.

hashtag
License

Since we're going to be putting our app out into the wild, we should include some information about who wrote it and the legal usage of its source code. For this we need a new file in our project's root folder: the LICENSE file. This file contains a copy of the license that your code is released under. For elementary OS apps this is typically the (GPL). Remember the header we added to our source code? That header reminds people that your app is licensed and it belongs to you. GitHub has a built-in way to add several popular licenses to your repository. Read their and add a LICENSE file to your repository.

circle-info

If you'd like to better understand software licensing, the Linux Foundation offers a

Next, we need a way to create a listing in AppCenter and a way to tell the interface to show our app in the Applications menu and dock: let's write some Metadata.

Continuous Integration

Continuous integration testing (also called CI), is a way to automatically ensure that your app builds correctly and has the minimum necessary configuration to run when it's installed. Setting up CI for your app can save you time and give you reassurance that when you submit your app for publishing it will pass the automated testing phase in AppCenter Dashboard. Keep in mind however, that there is also a human review portion of the submission process, so an app that passes CI may still need fixing before being published in AppCenter. Now that you have an app with a build system, metadata files, and packaging, we can configure CI using GitHub Actions.

  1. Navigate to your project's page on GitHub and select the "Actions" tab.

  2. Select "set up a workflow yourself". You'll then be shown the main.yml file with GitHub's default CI configuration. Replace the default workflow with the following:

  1. Select "Start commit" in the top right corner of the page to commit this workflow to your repository.

That's it! The Flatpak CI workflow will now run on your repository for any new commits or pull requests. You'll be able to see if the build succeeded or failed, and any reasons it might have failed. Configuring this CI workflow will help guide you during the development process and ensure that everything is working as intended.

This workflow will also produce a Flatpak Bundle file using GitHub Artifacts that you can download and install with Sideload. Using this bundle file, you can test feature or bug fix branches with your team, contributors, or community before merging the proposed fix into your main git branch.

GitHub Actions can be used to configure many more types of CI or other automation. For more information, check out the .

Markdown Badges

elementary provides badges, e.g. for your GitHub README. Badges will open to your app's page on the AppCenter website where users can learn more about your app, see screenshots, and decide to download it. They look like this:

hashtag
Markdown

Monetizing Your App

hashtag
Marking Your App for Monetization

At any time, you can select the "$" icon in your dashboard to link your Stripe accountarrow-up-right and enable payments for your app in AppCenter. However, this change will only apply to your app when a new release is submitted; to link a new account or unlink an account you will need to create a new release and submit it for publishing.

hashtag
Why Monetize?

Monetizing your app allows you to accept payments from users when they download your app. AppCenter displays the suggested price as the download button, along with a dropdown where users can choose their own price.

circle-info

Remember that AppCenter is pay-what-you-want; the suggested price is not a price floor, and users can always enter a different price, including $0

Even if you want to give your app away for free, you should consider monetizing and setting the suggested price to $0. This lists your app as free throughout AppCenter, but allows users to send you optional payments when installing and from your app's listing.

hashtag
Platform Fees

Each payment is split between you and elementary 70/30, with a minimum payment to elementary of $0.50 to cover distribution and payment processing. For example: with a $10 payment, $3 is paid to elementary and $7 is paid to you. For a $1 payment, $0.50 is paid to elementary to cover distribution and payment processing while $0.50 is paid to you.

There are no enrollment fees or subscription costs to publish your app in AppCenter.

Add a new Gtk.Label to the window with the text "Hello Again World!"
  • Finally, this time we're going to prefix our file with a small legal header. Make sure this header matches the license of your code and assigns copyright to you. More info about this later.

  • the last chapter
    GNU General Public Licensearrow-up-right
    documentation for adding software licensesarrow-up-right
    free online course on open source licensingarrow-up-right
    GitHub Actions websitearrow-up-right
    src/Application.vala
    /*
     * SPDX-License-Identifier: GPL-3.0-or-later
     * SPDX-FileCopyrightText: 2023 Your Name <[email protected]>
     */
     
     public class MyApp : Gtk.Application {
        public MyApp () {
            Object (
                application_id: "io.github.yourusername.yourrepositoryname",
                flags: ApplicationFlags.DEFAULT_FLAGS
            );
        }
    
        protected override void activate () {
            var label = new Gtk.Label ("Hello Again World!");
       
            var main_window = new Gtk.ApplicationWindow (this) {
                child = label,
                default_height = 300,
                default_width = 300,
                title = "Hello World"
            };
            main_window.present ();
        }
    
        public static int main (string[] args) {
            return new MyApp ().run (args);
        }
    }
    git remote add origin [email protected]:yourusername/yourrepositoryname.git
    git push -u origin master
    name: CI
    
    # This workflow will run for any pull request or pushed commit
    on: [push, pull_request]
    
    # A workflow run is made up of one or more jobs that can run sequentially or in parallel
    jobs:
      # This workflow contains a single job called "flatpak"
      flatpak:
        # The type of runner that the job will run on
        runs-on: ubuntu-latest
    
        # This job runs in a special container designed for building Flatpaks for AppCenter
        container:
          image: ghcr.io/elementary/flatpak-platform/runtime:8
          options: --privileged
    
        # Steps represent a sequence of tasks that will be executed as part of the job
        steps:
          # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it
        - uses: actions/checkout@v3
    
          # Builds your flatpak manifest using the Flatpak Builder action
        - uses: flatpak/flatpak-github-actions/flatpak-builder@v6
          with:
            # This is the name of the Bundle file we're building and can be anything you like
            bundle: MyApp.flatpak
            # This uses your app's RDNN ID
            manifest-path: io.github.yourusername.yourrepositoryname.yml
    
            # You can automatically run any of the tests you've created as part of this workflow
            run-tests: true
    
            # These lines specify the location of the elementary Runtime and Sdk
            repository-name: appcenter
            repository-url: https://flatpak.elementary.io/repo.flatpakrepo
            cache-key: "flatpak-builder-${{ github.sha }}"
    hashtag
    HTML
    circle-info

    Make sure to replace io.github.USER.REPO in the URL with the ID for your app

    [![Get it on AppCenter](https://appcenter.elementary.io/badge.svg)](https://appcenter.elementary.io/io.github.USER.REPO)
    arrow-up-right
    <a href="https://appcenter.elementary.io/io.github.USER.REPO"><img src="https://appcenter.elementary.io/badge.svg" alt="Get it on AppCenter"></a>
    optional fieldsarrow-up-right
    Open Age Rating Service (OARS)arrow-up-right
    a short surveyarrow-up-right
    AppCenter Dashboardarrow-up-right
    Publishing Updates
    this HIG entryarrow-up-right

    Translations

    Now that you've learned about Meson, the next step is to make your app able to be translated to different languages. The first thing you need to know is how to mark strings in your code as translatable. Here's an example:

    See the difference? We marked the string as translatable by adding _() around it. Go back to your project and make all your strings translatable by adding _().

    Now we have to make some changes to our Meson build system and add a couple new files to describe which files we want to translate and which languages we want to translate into.

    1. Open up your "meson.build" file and add these lines below your project declaration:

    2. Remove the lines that install your .desktop and metainfo files and replace them with the following:

      The merge_file method combines translating and installing files, similarly to how the executable method combines building and installing your app. You might have noticed that this method has both an input argument and an output argument. We can use this instead of the rename argument from the install_data method.

    3. Still in this file, add the following as the last line:

    4. You might have noticed in step 2 that the merge_file method has an input and output. We're going to append the additional extension .in to our .desktop and .metainfo.xml files so that this method can take the untranslated files and produce translated files with the correct names.

      We use the git mv command here instead of renaming in the file manager or with mv so that git can keep track of the file rename as part of our revision history.

    5. Now, Create a directory named "po" in the root folder of your project. Inside of your po directory you will need to create another "meson.build" file. This time, its contents will be:

    6. Inside of "po" create another file called "POTFILES" that will contain paths to all of the files you want to translate. For us, this looks like:

    7. Now it's time to go back to your build directory and generate some new files using Terminal! The first one is our translation template or .pot file:

      After running this command you should notice a new file in the po directory containing all of the translatable strings for your app.

    8. We have one more file to create in the "po" directory. This file will be named "LINGUAS" and it should contain the two-letter language codes for all languages you want to provide translations for. As an example, let's add German and Spanish

    9. Now we can use the template file to generate translation files for each of the languages we listed in the LINGUAS file with the following command:

      You should notice two new files in your po directory called de.po and es.po. These files are now ready for translators to localize your app!

    10. Last step. Don't forget to add all of the new files we created in the po directory to git:

    That's it! Your app is now fully ready to be translated. Remember that each time you add new translatable strings or change old ones, you should regenerate your .pot and po files using the -pot and -update-po build targets from the previous two steps. If you want to support more languages, just list them in the LINGUAS file and generate the new po file with the -update-potarget. Don't forget to add any new po files to git!

    circle-info

    If you're having trouble, you can view the full example code

    hashtag
    Translators Comments

    Sometimes detailed descriptions in the context of translatable strings are necessary for disambiguation or to help in the creation of accurate translations. For these situations use /// TRANSLATORS: comments.

    The Basic Setup

    Before we even think about writing code, you'll need a certain basic setup. This chapter will walk you through the process of getting set up. We will cover the following topics:

    • Creating an account on GitHub and importing an SSH key

    • Setting up the Git version control system

    • Getting and using the elementary developer "SDK"

    We’re going to assume that you’re working from a clean installation of elementary OS 5.1 Hera or later. This is important as the instructions you’re given may reference apps that are not present (or even available) in other GNU/Linux based operating systems like Ubuntu. It is possible to apply the principles of this guide to Ubuntu development, but it may be more difficult to follow along.

    hashtag
    GitHub

    GitHub is an online platform for hosting code, reporting issues, tracking milestones, making releases, and more. If you're planning to publish your app through AppCenter, you'll need a GitHub account. If you already have an account, feel free to move on to the next section. Otherwise, and return when you're finished.

    hashtag
    Git

    To download and upload to GitHub, you'll need the Terminal program git. Git is a type of that allows multiple developers to collaboratively develop and maintain code while keeping track of each version along the way.

    If you're ready, let's get you set up to use Git:

    1. Open the Terminal and install Git

    2. We need to inform Git who we are so that when we upload code it is attributed correctly. Inform Git of your identity with the following commands

    3. To authenticate and transfer code securely, you’ll need to generate an key pair (a kind of fingerprint for your computer) and import your public key to GitHub. Type the following in Terminal:

    We're all done! Now you can download source code hosted on GitHub and upload your own code. We'll revisit using git in a bit, but for now you're set up.

    circle-info

    For a more in-depth intro to Git, we recommend .

    For more details on uploading your SSH public key to GitHub, please see .

    hashtag
    Developer "SDK"

    At the time of this writing, elementary OS doesn't have a full SDK like Android or iOS. But luckily, we only need a couple apps to get started writing code.

    hashtag
    Code

    The first piece of our "SDK" is Code. This comes by default with elementary OS. It comes with some helpful features like syntax highlighting, auto-save, and a Folder Manager. There are other extensions for Code as well, like the Outline, Terminal, Word Completion, or Devhelp extensions. Play around with what works best for you.

    hashtag
    Terminal

    We’re going to use Terminal in order to compile our code, push revisions to GitHub (using git), and other good stuff. Throughout this guide, we’ll be issuing Terminal commands. You should assume that any command is executed from the directory “Projects” in your home folder unless otherwise stated. Since elementary OS doesn’t come with that folder by default, you’ll need to create it.

    Open Terminal and issue the following command:

    hashtag
    Development Libraries

    In order to build apps you're going to need their development libraries. We can fetch a basic set of libraries and other development tools with the following terminal command:

    hashtag
    Flatpak

    On elementary OS you will already have the required Flatpak remote and platform pre-installed. On other Linux OSes, you can add the remote and install the Flatpak platform and SDK:

    And with that, we're ready to dive into development! Let's move on!

    Creating Logs

    One way to debug apps is by logging information in the code. This enables seeing which code was run when a problem occurred and what the value of variables were. You can directly call GLib.Log ()arrow-up-right or use the convenience functions listed below.

    circle-info

    To view logs from all your applications you can use journalctlarrow-up-right.

    hashtag
    Debug

    Debug logs usually give detailed information on the flow through the system and are not printed to Terminal or logs by default.

    Log functions, like debug use style formatting and can be called like this:

    circle-info

    Start your app with G_MESSAGES_DEBUG=all to print debug messages

    hashtag
    Info

    Use info log level to log informational messages as well as interesting runtime events. These logs are also immediately visible on a status console, and should be kept to a minimum.

    hashtag
    Message

    Use the message log level to output a message.

    hashtag
    Warning

    The warning log level outputs messages that warn of, for example, use of deprecated APIs, 'almost' errors, or runtime situations that are undesirable or unexpected, but not necessarily "wrong". These logs are immediately visible on a status console.

    circle-info

    Start your app with G_DEBUG=fatal-warnings to exit the program at the first call to warning () or critical ()

    hashtag
    Critical

    Critical log level is used when there is a severe application failure that should be investigated immediately.

    circle-info

    Start your app with G_DEBUG=fatal-criticals to exit the program at the first call to critical ()

    hashtag
    Error

    Error log level includes logs for runtime errors or unexpected conditions. These errors are immediately visible on a status console and cause your app to exit.

    The Build System

    Compiling Binaries & Installing Data with Meson

    The next thing we need is a build system. The build system that we're going to be using is called . We already installed the meson program at the beginning of this book when we installed elementary-sdk. What we're going to do in this step is create the files that tell Meson how to install your program. This includes all the rules for building your source code as well as correctly installing your icons, .desktop, and metainfo files and the binary app that results from the build process.

    Create a new file in your project's root folder called "meson.build". We've included some comments along the way to explain what each section does. You don't have to copy those, but type the rest into that file:

    Notice that in each of our install_data methods, we rename our files using our project name. By using our project name—and its RDNN scheme—we will ensure that our files are installed under a unique name that won't cause conflicts with other apps.

    Submission Process

    The submission process for AppCenter, broken down into a few short steps

    circle-info

    Please make sure to review before submitting your app

    hashtag
    Create a New Release

    <?xml version="1.0" encoding="UTF-8"?>
    <component type="desktop-application">
      <id>io.github.myteam.myapp</id>
    
      <name>My App</name>
      <summary>Proves that we can use Vala and Gtk</summary>
    
      <metadata_license>CC-BY-4.0</metadata_license>
      <project_license>GPL-3.0-or-later</project_license>
    
      <description>
        <p>
          A quick summary of your app's main selling points and features. Just a couple sentences per paragraph is best
        </p>
      </description>
    
      <launchable type="desktop-id">io.github.myteam.myapp.desktop</launchable>
    </component>
    <screenshots>
      <screenshot type="default">
        <caption>The most important feature of my app</caption>
        <image>https://raw.githubusercontent.com/myteam/myapp/1.0.0/data/screenshot.png</image>
      </screenshot>
    </screenshots>
    <branding>
      <color type="primary">#f37329</color>
    </branding>
    <custom>
      <value key="x-appcenter-suggested-price">5</value>
      <value key="x-appcenter-stripe">pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</value>
    </custom>
    <releases>
      <release version="1.0.0" date="2022-09-08">
        <description>
          <p>Initial release, includes features such as:</p>
          <ul>
            <li>Application can be launched</li>
          </ul>
        </description>
      </release>
    </releases>
    [Desktop Entry]
    Version=1.0
    Type=Application
    
    Name=My App
    Comment=Proves that we can use Vala and Gtk
    Categories=Development;Education;
    
    Icon=io.github.myteam.myapp
    Exec=io.github.myteam.myapp
    Terminal=false
    git add data/
    git commit -am "Add Metadata files"
    git push
    stdout.printf ("Not Translatable string");
    stdout.printf (_("Translatable string!"));
    
    string normal = "Another non-translatable string";
    string translated = _("Another translatable string");
    Get it on AppCenter
    here on GitHubarrow-up-right
    printfarrow-up-right
    And you're done! Your app now has a real build system. This is a major milestone in your app's development!
    circle-info

    Don't forget to add these files to git and push to GitHub.

    hashtag
    Building and Installing with Meson

    Now that we have a build system, let's try it out. Configure the build directory using the Meson command in Terminal:

    This command tells Meson to get ready to build our app using the prefix "/usr" and that we want to build our app in a clean directory called "build". The meson setup command defaults to installing our app locally, but we want to install our app for all users on the computer.

    Change into the build directory and use ninja to build. Then, if the build is successful, install with ninja install:

    If all went well, you should now be able to open your app from the Applications Menu and pin it to the Dock. We'll revisit Meson again later to add some more complicated behavior, but for now this is all you need to know to give your app a proper build system. If you want to explore Meson a little more on your own, you can always check out Meson's documentationarrow-up-right.

    circle-exclamation

    If you were about to add the "build" folder to your git repository and push it, stop! This binary was built for your computer and we don't want to redistribute it. In fact, we built your app in a separate folder like this so that we can easily delete or ignore the "build" folder and it won't mess up our app's source code.

    hashtag
    Uninstalling the application

    To uninstall your application, change into the build directory and we will use ninja once again to uninstall:

    If all went well, you should see command output that shows files related to your application were removed. Again, more details can be found in Meson's documentationarrow-up-right.

    hashtag
    Review

    Let's review all we've learned to do:

    • Create a new Gtk app using Gtk.Window, Gtk.Button, and Gtk.Label

    • Keep our projects organized into branches

    • License our app under the GPL and declare our app's authors in a standardized manner

    • Create a .desktop file using RDNN that tells the computer how to display our app in the Applications Menu and the Dock

    • Set up a Meson build system that contains all the rules for building our app and installing it cleanly

    • Uninstall our application cleanly

    That's a lot! You're well on your way to becoming a bona fide app developer for elementary OS. Give yourself a pat on the back, then take some time to play around with this example. Change the names of files and see if you can still build and install them properly. Ask another developer to clone your repo from GitHub and see if it builds and installs cleanly on their computer. If so, you've just distributed your first app! When you're ready, we'll move onto the next section: Translations.

    circle-info

    If you're having trouble, you can view the full example code here on GitHubarrow-up-right

    Mesonarrow-up-right
    # Include the translations module
    i18n = import('i18n')
    
    # Set our translation domain
    add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c')
    #Translate and install our .desktop file
    i18n.merge_file(
        input: 'data' / 'hello-again.desktop.in',
        output: meson.project_name() + '.desktop',
        po_dir: meson.project_source_root() / 'po',
        type: 'desktop',
        install: true,
        install_dir: get_option('datadir') / 'applications'
    )
    
    #Translate and install our .metainfo file
    i18n.merge_file(
        input: 'data' / 'hello-again.metainfo.xml.in',
        output: meson.project_name() + '.metainfo.xml',
        po_dir: meson.project_source_root() / 'po',
        install: true,
        install_dir: get_option('datadir') / 'metainfo'
    )
    subdir('po')
    git mv data/hello-again.desktop data/hello-again.desktop.in
    git mv data/hello-again.metainfo.xml data/hello-again.metainfo.xml.in
    i18n.gettext(meson.project_name(),
        args: '--directory=' + meson.project_source_root(),
        preset: 'glib'
    )
    src/Application.vala
    data/hello-again.desktop.in
    data/hello-again.metainfo.xml.in
    ninja io.github.yourusername.yourrepositoryname-pot
    de
    es
    ninja io.github.yourusername.yourrepositoryname-update-po
    git add src/Application.vala meson.build po/ data/
    git commit -am "Add translations"
    git push
    /// TRANSLATORS: The first %s is search term, the second is the name of default browser
    title = _("Search for %s in %s").printf (query, get_default_browser_name ());
    string name = "Bob";
    int age = 30;
    debug ("Person: %s %i", name, age);
    info ("An event occurred");
    message ("An event occurred");
    warning ("Something potentially problematic happened!");
    critical ("A major issue occurred! Uh oh!");
    error ("Some catastrophic happened and the app had to exit!");
    # project name, programming language, and required meson version
    project('io.github.yourusername.yourrepositoryname',
        'vala', 'c',
        meson_version: '>= 0.56.0'
    )
    
    # Create a new executable, list the files we want to compile, list the dependencies we need, and install
    executable(
        meson.project_name(),
        'src' / 'Application.vala',
        dependencies: [
            dependency('gtk4')
        ],
        install: true
    )
    
    # Install our .desktop file so the Applications Menu will see it
    install_data(
        'data' / 'hello-again.desktop',
        install_dir: get_option('datadir') / 'applications',
        rename: meson.project_name() + '.desktop'
    )
    
    # Install our .metainfo.xml file so AppCenter will see it
    install_data(
        'data' / 'hello-again.metainfo.xml',
        install_dir: get_option('datadir') / 'metainfo',
        rename: meson.project_name() + '.metainfo.xml'
    )
    meson setup build --prefix=/usr
    cd build
    ninja
    ninja install
    sudo ninja uninstall

    When prompted, press Enter to accept the default file name for your key. You can choose to protect your key with a password or press Enter again to use no password when pushing code.

  • Now we're going to import your public key to GitHub. View your public SSH key with the following command, then copy the text that appears

  • Visit your SSH keys pagearrow-up-right and click the green button in the upper right-hand corner that says "New SSH key". Paste your key in the "Key" box and give it a title.

  • sign up for a GitHub accountarrow-up-right
    version control systemarrow-up-right
    SSHarrow-up-right
    Codecademy's course on Gitarrow-up-right
    GitHub's official guidearrow-up-right
    Code icon
    Terminal icon
    Generic application icon
    Before you can publish your app, you'll need to release it on GitHubarrow-up-right. It is important that you use a Semantic Versioning Numberarrow-up-right without a pre-release tag when releasing.

    hashtag
    Submit for Review

    triangle-exclamation

    It is important to make changes to your app's monetization status before completing this step

    To submit your app to AppCenter, you'll need to create a new JSON file describing your app release in the AppCenter Reviews repository on Github:

    Name the file using your app's id with the json file extension. Fill in the contents of the file with the link to your git repository, the commit hash from your latest release, and the version number of that release:

    The source field needs to be a publicly accessible Git repository. The commit field is the full Git sha for the release you're submitting. And the version field needs to be a SemVerarrow-up-right tag.

    Once you've submitted the new file as a pull request to the AppCenter Reviews repository, your app will now be in the review queue!

    hashtag
    Wait for Testing

    All apps and updates in the review queue are listed as open pull requestsarrow-up-right. Once a pull request is approved and merged by a reviewer, that app will be published to the AppCenter Flatpak repositoryarrow-up-right. Depending on demand, this may take several days.

    hashtag
    Resolve Issues

    Your app will go through an automated build process when it has been accepted by a reviewer. It will need to pass the Parse and Build steps listed in GitHub CI at the bottom of the pull request. If either of those steps fail, please inspect the build log to resolve issues.

    If any questions or requested changes come up during the review process, the reviewer will use the conversation on the pull request and may mark it as "Changes Requested". You will need to correct these issues, create a new release of your app, and update the pull request with your new release.

    The reviewer may also make some suggestions for best practices, inform you of new features, or link you to documentation.

    hashtag
    Available on AppCenter

    When your app passes automated testing and human review, it will become available in AppCenter in elementary OS.

    hashtag
    Publishing Updates

    If you wish to publish an update, you can restart the submission process by creating a new release on GitHub. Then navigate to the AppCenter Reviews Github repositoryarrow-up-right, modify your app's json file with the latest commit hash and release tag, and submit that change as a pull request to begin a new review process.

    the publishing requirements

    Packaging

    While having a build system is great, our app still isn't ready for regular users. We want to make sure our app can be built and installed without having to use Terminal. What we need to do is package our app. To do this, we use Flatpak on elementary OS. This section will teach you how to package your app as a Flatpak, which is required to publish apps in AppCenter. This will allow people to install your app and even get updates for it when you publish them.

    hashtag
    Practice Makes Perfect

    If you want to get really good really fast, you're going to want to practice. Repetition is the best way to commit something to memory. So let's recreate our entire Hello World app again from scratch:

    1. Create a new branch folder "hello-packaging"

    2. Set up our directory structure including the "src" and "data" folders.

    3. Add your LICENSE, .desktop, .metainfo.xml, icons, and source code.

    4. Now set up the Meson build system and translations.

    5. Test everything!

    Did you commit and push to GitHub for each step? Keep up these good habits and let's get to packaging this app!

    hashtag
    Flatpak Manifest

    The Flatpak manifest file describes your app's build dependencies and required permissions. Create a io.github.yourusername.yourrepositoryname.yml file in your project root with the following contents:

    To run a test build and install your app, we can use flatpak-builder with a few arguments:

    This tells Flatpak Builder to build the manifest we just wrote into a clean build folder the same as we did for Meson. Plus, we install the built Flatpak package locally for our user. If all goes well, congrats! You've just built and installed your app as a Flatpak.

    That wasn't too bad, right? We'll set up more complicated packaging in the future, but this is all that is required to submit your app to AppCenter Dashboard for it to be built, packaged, and distributed. If you'd like you can always read .

    hashtag
    Uninstalling the application

    Ninja and Flatpak both provide commands to uninstalling the application. It is recommended to use the provided method in both cases.

    To remove our application with Flatpak, we will use Flatpak's remove command:

    Flatpak will prompt you to remove your application.

    Note: You can append -y to the command to bypass the dialog confirmation prompt

    If you'd like you can always read .

    Hello World

    The first app we’ll create will be a basic and generic “Hello World”. We’ll walk through the steps of creating folders to store our source code, compiling our first app, and pushing the project to a Git branch. Let’s begin.

    hashtag
    Setting Up

    Apps on elementary OS are organized into standardized directories contained in your project's "root" folder. Let's create a couple of these to get started:

    1. Create your root folder called "gtk-hello"

    2. Create a folder inside that one called "src". This folder will contain all of our source code.

    Later on, We'll talk about adding other directories like "po" and "data". For now, this is all we need.

    hashtag
    Gtk.Application

    Now what you've been waiting for! We're going to create a window that contains a button. When pressed, the button will display the text "Hello World!" To do this, we're going to use a widget toolkit called GTK and the programming language Vala. Before we begin, we highly recommend that you do not copy and paste. Typing each section manually will help you to practice and remember. Let's get started:

    Create a new file in Code and save it as "Application.vala" inside your "src" folder

    In this file, we're going to create a special class called a Gtk.Application. Gtk.Application is a class that handles many important aspects of a Gtk app like uniqueness and the ID you need to identify your app to the notifications server. If you want some more details about Gtk.Application, . For now, type the following in "Application.vala".

    You'll notice that most of these property names are pretty straightforward. Inside MyApp () we set a couple of properties for our Gtk.Application object, namely our app's ID and . The naming scheme we used for our app's ID is called and will ensure that your app has a unique identifier. The first line inside the activate method creates a new Gtk.ApplicationWindow called main_window. The fourth line sets the window title that you see at the top of the window. We also must give our window a default size so that it does not appear too small for the user to interact with it. Then in our main () method we create a new instance of our Gtk.Applicationand run it.

    Ready to test it out? Fire up your terminal and make sure you're in "~/Projects/gtk-hello/src". Then execute the following commands to compile and run your first Gtk app:

    Do you see a new, empty window called "Hello World"? If so, congratulations! If not, read over your source code again and look for errors. Also check the output of your terminal. Usually there is helpful output that will help you track down your mistake.

    Now that we've defined a nice window, let's put a button inside of it. Add the following to your application at the beginning of the activate () function:

    Then add this line right before main_window.present ():

    Any ideas about what happened here?

    • We've created a new Gtk.Button with the label "Click me!"

    • Then we add margins to the button so that it doesn't bump up against the sides of the window.

    • We've said that if this button is clicked, we want to change the label to say "Hello World!" instead.

    Compile and run your application one more time and test it out. Nice job! You've just written your first GTK app!

    circle-info

    If you're having trouble, you can view the full example code

    hashtag
    Pushing to GitHub

    After we do anything significant, we must remember to push our code. This is especially important in collaborative development where not pushing your code soon enough can lead to unintentional forks and pushing too much code at once can make it hard to track down any bugs introduced by your code.

    First we need to create a new repository on GitHub. Visit and create a new repository for your code.

    Open Terminal and make sure you're in your project's root directory "~/Projects/gtk-hello", then issue the following commands

    With these commands:

    • We've told git to track revisions in this folder

    • That we'd like to track revisions on the file "Application.vala" specifically

    • We've committed our first revision and explained what we did in the revision

    circle-info

    Remember to replace "yourusername" with your GitHub username and "yourrepositoryname" with the name of the new repository you created

    hashtag
    Victory!

    Let's recap what we've learned to do in this first section:

    • We created a new project containing a "src" folder

    • We created our main vala file and inside it we created a new Gtk.Window and Gtk.Button

    • We built and ran our app to make sure that everything worked properly

    Feel free to play around with this example. Make the window a different size, set different margins, make the button say other things. When you're comfortable with what you've learned, go on to the next section.

    hashtag
    A Note About Libraries

    Remember how when we compiled our code, we used the valac command and the argument --pkg gtk4? What we did there was make use of a "library". If you're not familiar with the idea of libraries, a library is a collection of methods that your program can use. So this argument tells valac to include the GTK library (version 4) when compiling our app.

    In our code, we've used the Gtk "Namespace" to declare that we want to use methods from GTK (specifically, Gtk.Window and Gtk.Button.with_label). Notice that there is a hierarchy at play. If you want to explore that hierarchy in more detail, you can .

    Color Scheme

    Setting a dark or light style for your app

    elementary OS ships with two styles for app widgets: a dark style and a light style. By default, your app will use the light style, but by using Gtk.Settings and Granite.Settings you can choose to always use a dark style or to follow the user's preference.

    hashtag
    Setting a Dark Style

    A dark styled app

    Some apps, like photo or video editors, benefit from reducing the contrast between their content and the app's UI by always choosing to be displayed using a dark style. You can set the dark style for your app by using Gtk.Settings and setting the property gtk_application_prefer_dark_theme. In your Application class, add the following lines to your activate function:

    Here, we get the default Gtk.Settings object and then we set this property to instruct GTK to use the dark style. Build your app and run it and notice that it now uses a dark style instead of a light style.

    hashtag
    Using The User's Preference

    Many apps will be usable in either a light or dark style. In this case, apps should respect the user's style preference and adapt when it is changed.

    First, make sure you've included Granite in the build dependencies declared in your meson.build file:

    Now, you can read and respond to the user's style preference with Granite.Settings and then use Gtk.Settings to set it in your app's activate function.

    System Settings

    Deep linking to System Settings

    Some of the features of your application may depend on system level settings or services, such as Internet connectivity or app level access to the network. While your app cannot directly change those settings, it can prompt users to change them by linking to specific panes in the System Settings.

    elementary OS implements a special settings:// URI scheme for linking to System Settings. The links lead to specific settings panes. You can find most commonly used ones in the Granite.SettingsUri namespace in the Granite library.

    hashtag
    Opening a link

    First, make sure you've included recent enough version of Granite and Gtk in the build dependencies declared in your meson.build file. You can get those dependencies by using the elementary Flatpak runtime version 7.2 or newer:

    Then you can open Network settings by launching Granite.SettingsUri.NETWORK:

    You can find out more about the Gtk.UriLauncher in .

    hashtag
    Use cases

    There are roughly two categories of settings that you can prompt users to change: general purpose and app specific. General ones include:

    • Granite.SettingsUri.NETWORK, where connection to the Internet can be enabled

    • Granite.SettingsUri.ONLINE_ACCOUNTS, where users can add their CalDAV and IMAP accounts

    • Granite.SettingsUri.SOUND_INPUT

    while app specific ones are:

    • Granite.SettingsUri.PERMISSIONS, where are managed

    • Granite.SettingsUri.LOCATION, where access to location data can be granted

    • Granite.SettingsUri.NOTIFICATIONS

    Icons

    The last thing we need for a minimum-viable-app is to provide app icons. Apps on elementary OS provide icons hinted in 6 sizes: 16px, 24px, 32px, 48px, 64px, and 128px. These icons will be shown in many places throughout the system, such as those listed below:

    Size
    AppCenter
    Applications Menu
    Dock
    Menus & Popovers
    Multitasking
    Notifications

    GObject-style Construction

    When creating a new class, prefer GObject-style construction. This type of class construction separates handling arguments from the main logic of your class. It also ensures that information passed in during construction is still available later, and makes your class more likely to be compatible with GLib's functions like property binding.

    A simple class written with GObject-style construction looks like this:

    hashtag
    Construction Properties

    In the above example, MyClass

    cat ~/.ssh/id_ed25519.pub
    sudo apt install git
    git config --global user.name "Your Name"
    git config --global user.email "[email protected]"
    ssh-keygen -t ed25519 -C "[email protected]"
    mkdir Projects
    sudo apt install elementary-sdk
    flatpak install -y appcenter io.elementary.Sdk
    flatpak remote-add --if-not-exists --system appcenter https://flatpak.elementary.io/repo.flatpakrepo
    flatpak install -y appcenter io.elementary.Platform io.elementary.Sdk
    io.github.myteam.myapp.json
    {
      "source": "https://github.com/danirabbit/harvey.git",
      "commit": "45dd52660c0069207fa4b0c1fcf7ac6d7157d34b",
      "version": "1.0.1"
    }
    more about Flatpakarrow-up-right
    more about Flatpak removearrow-up-right

    We've also said that we want to make the button insensitive after it's clicked; We do this because clicking the button again has no visible effect

  • Finally, we add the button to our Gtk.ApplicationWindow and declare that we want to show the window.

  • Then we've told git to push your code to GitHub.

    Finally, we committed our first revision and pushed code to GitHub

    check out Valadocarrow-up-right
    flagsarrow-up-right
    Reverse Domain Name Notationarrow-up-right
    here on GitHubarrow-up-right
    the new repository pagearrow-up-right
    check out Valadocarrow-up-right
    , where microphone input can be enabled
    , where presentation of app notifications can be customized
  • Granite.SettingsUri.SHORTCUTS, where global keyboard shortcuts can be added

  • its documentationarrow-up-right
    Flatpak permissionsarrow-up-right
    runtime: io.elementary.Platform
    runtime-version: '7.2'
    sdk: io.elementary.Sdk
    executable(
        meson.project_name(),
        'src' / 'Application.vala',
        dependencies: [
            dependency('granite-7', version: '>= 7.3.0'),
            dependency('gtk4', version: '>= 4.10.0')
        ],
        install: true
    )

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    24px

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    32px

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    48px

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    64px

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✖️ No

    128px

    ✔️ Yes

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    circle-info

    To help you provide the necessary sizes—and for this tutorial—Micah Ilbery maintains an icon template project here on GitHubarrow-up-right

    Place your icons in the data directory and name them after their pixel sizes, such as32.svg, 64.svg, etc. The file structure should look like this:

    Now that you have icon files in the data directory, add the following lines to the end of meson.build to install them .

    You'll notice the section for installing app icons is a little more complicated than installing other files. In this example, we're providing SVG icons in all of the required sizes for AppCenter and, since we're using SVG, we're installing them for both LoDPI and HiDPI. If you're providing PNG icons instead, you'll need to tweak this part a bit to handle assets exported for use on HiDPI displays.

    If you cannot see your new icon in the Applications Menu or the Dock once you've reinstalled your app, refresh your system's icon cache using the following command:

    circle-info

    For more information about creating and hinting icons, check out the Human Interface Guidelinesarrow-up-right

    16px

    ✖️ No

    has a
    public
    property
    foo
    whose type is
    int
    , but we've also declared it as
    { get; construct; }
    . This shorthand declares the type of access for this property's
    get
    and
    set
    functions. Since we declared the property as
    public
    ,
    get
    is also public, but we've declared the
    set
    function as
    construct
    , which means we can only assign its value as a constructor argument.

    We want construction arguments to be public to ensure that we can later get information out of a class that was used to construct it, if need be. But it's important to declare limited set access on properties for future maintainability. Since there is no handling for changes in this property in the construct block, setting this property after the class is constructed would have no effect, even if we allowed it by declaring { get; construct set; }.

    hashtag
    Constructors

    MyClass also contains a constructor; it describes what arguments are required to construct the class. We've declared here that in order to construct MyClass, we need an int passed in when we initialize a new object such as with var new_class = new MyClass (5);

    Inside the constructor, we have a special Object () call, in which we specify a property and its value, Object (foo: foo);. This sets the value of the integer property foo defined earlier to the value received as an argument. It is equivalent to saying this.foo = foo;.

    hashtag
    The Construct Block

    When using GObject-style construction, a constructor should only contain code that parses arguments and sets property values with the Object () call. All other class construction logic happens in the construct block. Code in the construct block runs every time an instance of this object is created, regardless of the constructor used.

    hashtag
    Declaring Multiple Constructors

    To see how this style of class construction scales and keeps code organized, let's look at a more complex example. Say we want to display a list of devices. Each device lives in its own row, which is denoted by the class Row. In this example, sometimes we have access to a Device object which stores the name and icon properties of the device, but for some devices we have to pass in those properties manually to construct our row. We can achieve this by declaring two constructors:

    • a default constructor that takes two arguments: string name and Icon icon

    • a separate, named constructor which takes one argument: a Device object

    Note that in both cases, we handle the arguments in the constructor, while the actual logic of creating UI widgets lives in the common construct block.

    This way, we can have multiple constructors without having to repeat the initialization logic.

    hashtag
    See also

    • GObject-style Construction section in the Vala Tutorialarrow-up-right

    # This is the same ID that you've used in meson.build and other files
    id: io.github.yourusername.yourrepositoryname
    
    # Instead of manually specifying a long list of build and runtime dependencies,
    # we can use a convenient pre-made runtime and SDK. For this example, we'll be
    # using the runtime and SDK provided by elementary.
    runtime: io.elementary.Platform
    runtime-version: '8'
    sdk: io.elementary.Sdk
    
    # This should match the exec line in your .desktop file and usually is the same
    # as your app ID
    command: io.github.yourusername.yourrepositoryname
    
    # Here we can specify the kinds of permissions our app needs to run. Since we're
    # not using hardware like webcams, making sound, or reading external files, we
    # only need permission to draw our app on screen using either X11 or Wayland.
    finish-args:
      - '--share=ipc'
      - '--socket=fallback-x11'
      - '--socket=wayland'
    
    # This section is where you list all the source code required to build your app.
    # If we had external dependencies that weren't included in our SDK, we would list
    # them here.
    modules:
      - name: yourrepositoryname
        buildsystem: meson
        sources:
          - type: dir
            path: .
    flatpak-builder build io.github.yourusername.yourrepositoryname.yml --user --install --force-clean
    flatpak remove io.github.yourusername.yourrepositoryname
    flatpak uninstall io.github.yourusername.yourrepositoryname --delete-data
    
    
            ID                                                          Branch           Op
     1.     io.github.yourusername.yourrepositoryname                   master           r
     2.     io.github.yourusername.yourrepositoryname.Locale            master           r
    
    Proceed with these changes to the user installation? [Y/n]:
    public class MyApp : Gtk.Application {
        public MyApp () {
            Object (
                application_id: "io.github.yourusername.yourrepositoryname",
                flags: ApplicationFlags.DEFAULT_FLAGS
            );
        }
    
        protected override void activate () {
            var main_window = new Gtk.ApplicationWindow (this) {
                default_height = 300,
                default_width = 300,
                title = "Hello World"
            };
            main_window.present ();
        }
    
        public static int main (string[] args) {
            return new MyApp ().run (args);
        }
    }
    valac --pkg gtk4 Application.vala
    ./Application
    var button_hello = new Gtk.Button.with_label ("Click me!") {
        margin_top = 12,
        margin_bottom = 12,
        margin_start = 12,
        margin_end = 12
    };
    
    button_hello.clicked.connect (() => {
        button_hello.label = "Hello World!";
        button_hello.sensitive = false;
    });
    main_window.child = button_hello;
    git init
    git add src/Application.vala
    git commit -m "Create initial structure. Create window with button."
    git remote add origin [email protected]:yourusername/yourrepositoryname.git
    git push -u origin master
    protected override void activate () {
        var gtk_settings = Gtk.Settings.get_default ();
        gtk_settings.gtk_application_prefer_dark_theme = true;
    }
    executable(
        meson.project_name(),
        'src/Application.vala',
        dependencies: [
            dependency('granite-7'),
            dependency('gtk4')
        ],
        install: true
    )
    protected override void activate () {
        // First we get the default instances for Granite.Settings and Gtk.Settings
        var granite_settings = Granite.Settings.get_default ();
        var gtk_settings = Gtk.Settings.get_default ();
    
        // Then, we check if the user's preference is for the dark style and set it if it is
        gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK;
    
        // Finally, we listen to changes in Granite.Settings and update our app if the user changes their preference
        granite_settings.notify["prefers-color-scheme"].connect (() => {
            gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK;
        });
    }
    var button = new Gtk.Button.with_label ("Change network settings");
    button.clicked.connect (() => {
        var launcher = new Gtk.UriLauncher (Granite.SettingsUri.NETWORK);
        launcher.launch.begin(null, null, (obj, res) => {
            try {
                launcher.launch.end (res);
            } catch (Error e) {
                warning ("Failed to open network settings: %s", e.message);
            }
        });
    });
    hello-again
        data
            16.svg
            24.svg
            32.svg
            48.svg
            64.svg
            128.svg
    # Install our icons in all the required sizes
    icon_sizes = ['16', '24', '32', '48', '64', '128']
    
    foreach i : icon_sizes
        install_data(
            'data' / i + '.svg',
            install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps',
            rename: meson.project_name() + '.svg'
        )
        install_data(
            'data' / i + '.svg',
            install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps',
            rename: meson.project_name() + '.svg'
        )
    endforeach
    sudo update-icon-caches /usr/share/icons/*
    public class MyClass : Object {
        public int foo { get; construct; }
    
        public MyClass (int foo) {
            Object (foo: foo);
        }
    
        construct {
            if (foo > 0) {
                // Do stuff
            } else {
                // Do other stuff
            }
        }
    }
    public int foo { get; construct; }
    public MyClass (int foo) {
        Object (foo: foo);
    }
    construct {
        if (foo) {
            // Do stuff
        } else {
            // Do other stuff
        }
    }
    public class Row : Object {
        public string name { get; construct; }
        public Icon icon { get; construct; }
    
        public Row (string name, Icon icon) {
            Object (
                name: name,
                icon: icon
            );
        }
    
        public Row.from_device (Device device) {
            Object (
                name: device.name,
                icon: device.icon
            );
        }
    
        construct {
            var icon = new Gtk.Image.from_gicon (icon, Gtk.IconSize.DND);
            var label = new Gtk.Label (name);
        }
    }
    public Row (string name, Icon icon) {
        Object (
            name: name,
            icon: icon
        );
    }
    
    public Row.from_device (Device device) {
        Object (
            name: device.name,
            icon: device.icon
        );
    }
    construct {
        var icon = new Gtk.Image.from_gicon (icon);
        var label = new Gtk.Label (name);
    }

    Boxes and Grids

    How to organize widgets using common containers

    Now that you know how to code, build, and package an app using Vala, Gtk, Meson, and Flatpak, it’s time to learn a little bit more about how to build out your app into something really useful. The first thing we need to learn is how to lay out widgets in our window. But we have a fundamental problem: We can only add one widget (one “child”) to Gtk.Window. So how do we get around that to create complex layouts in a Window? We have to add a widget that can contain multiple children. The most common widgets for creating layouts are Boxes and Grids.

    hashtag
    Gtk.Box

    A Box is a widget that can contain multiple children in a single row or column. We create a new Gtk.Box like this:

    Remember that Gtk.Button and Gtk.Label accept an argument (a String) in the creation method (that’s the stuff in parentheses and quotes). As shown above, Gtk.Box accepts two arguments in the creation method— and spacing. Here, we’ve declared that when we add widgets to our box, they should stack vertically.

    Let’s add some stuff to the Box:

    Add the box to a window using the child property:

    Now build your app and see what it looks like. Since we’ve given our box a Gtk.Orientation of VERTICAL the labels stack up on top of each other. Try creating a Gtk.Box with horizontal orientation.

    hashtag
    Gtk.Grid

    While we can use Gtk.Box to create single row or single column layouts with the append method, we can't use it to create row-and-column-based layouts. Instead, we can use Gtk.Grid:

    Notice that the attach method takes 5 arguments:

    1. The widget that you want to attach to the grid.

    2. The column number to attach to starting at 0.

    3. The row number to attach to starting at 0.

    You can also use attach_next_to to place a widget next to another one on . Note also that providing the number of rows and columns the widget should span is optional. If you supply only the column and row numbers, Gtk will assume that the widget will span 1 column and 1 row.

    circle-info

    Make sure to give widgets unique names that you’ll remember. It’s best practice to use descriptive names so that people who are unfamiliar with your code can understand what a widget is for without having to know your app inside and out.

    hashtag
    Review

    Let’s recap what we learned in this section:

    • We packed multiple children into a Window using Gtk.Box and Gtk.Grid

    • We set the properties of Gtk.Grid including its spacing

    Now that you understand more about Boxes and Grids, try packing other kinds of widgets into a window like an Image. Don’t forget to play around with the attach method and widgets that span across multiple rows and columns. Remember that Valadoc is super helpful for learning more about the methods and properties associated with widgets.

    State Saving

    Apps should automatically save their state in elementary OS, allowing a user to re-open a closed app and pick up right where they left off. To do so, we utilize GSettings via GLib.Settingsarrow-up-right. GSettings allows your app to save certain stateful information in the form of booleans, strings, arrays, and more. It's a great solution for window size and position as well as whether certain modes are enabled or not. Note that GSettings is ideal for small amounts of configuration or stateful data, but user data (i.e. documents) should be stored on the disk.

    For the simplest example, let's create a switch in your app, and save its state.

    hashtag
    Define Settings Keys

    First, you need to define what settings your app will use so they can be accessed by your app. In your data/ folder, create a new file named gschema.xml. Inside it, let's define a key for the switch:

    The schema's path and id attributes are your app's ID (path in a / format while id is the standard dot-separated format). Note the key's name and type attributes: the name is a string to reference the setting, while in this case type="b" defines the setting as a boolean. The key's summary and description are developer-facing and are exposed in developer tools like dconf Editor.

    hashtag
    Use The Settings Object

    In order to interact with the settings key we just defined, we need to create a GLib.Settings object in our application. Building on previous examples, you can create a Gtk.Switch, add it to your window, create a Settings object, and bind the switch to a settings key like so:

    You can read more about , but for now this will bind the active property of the switch to the value of useless-setting in GSettings. When one changes, the other will stay in sync to reflect it.

    hashtag
    Install and Compile Schemas with Meson

    We need to add the new GSchema XML file to our build system so it is included at install time. Create a file data/meson.build and type the following:

    This ensures your gschema.xml file is installed, and renames it with your app's ID to avoid filename conflicts.

    Be sure to add the following lines to the end of the meson.build file in your source root so that Meson will compile the installed schemas:

    Compile and install your app to see it in action!

    To see the state saving functionality in action, you can open your app, toggle the switch, close it, then re-open it. The switch should retain its previous state. To see it under the hood, open dconf Editor, navigate to your app's settings at io.github.yourusername.yourrepositoryname, and watch the useless-setting update in real time when you toggle your switch.

    circle-info

    If you're having trouble, you can view the full example code

    Publishing Updates

    hashtag
    Updating Screenshots

    Screenshots are uploaded to the AppCenter screenshot server at publication time. The URL referenced in your MetaInfo file will be automatically replaced during publication and any changes you make to images hosted at the remote URL will not be reflected in AppCenter.

    circle-info

    If you change the UI of your app in an update, you must update your screenshots

    hashtag
    Release Notes

    Release notes show up in AppCenter both on your app info page and on the updates page.

    hashtag
    Descriptions

    The contents of the description tag should be written for the people who use your app. Avoid technical language and developer-facing changes and focus more on what people can expect to see in this update.

    hashtag
    ✅️ Good Example

    hashtag
    ❌️ Poor Example

    hashtag
    Issues

    You can also list issues from your issue tracker (e.g. GitHub) that have been resolved in this release as a way for you to close the feedback loop that began when someone opened an issue on your tracker. Issue tags will be presented as links below a release description in AppCenter. Showing that you are responsive to feedback is a powerful tool in building a community around your app.

    For now only the generic issue type is supported, and you must provide the url property. The value of the tag should be the title of the issue that it links to. For example:

    circle-info

    For more information on formatting releases, see the .

    hashtag
    GitHub User/Org Name Changes

    AppCenter does not support changing your GitHub username or organization name, as we rely on the unique RDNN of your app to avoid conflicts—and there is not an automated way to change the RDNN throughout the whole stack from AppCenter Dashboard down to the .deb files built and hosted in the AppCenter repository. If you change your GitHub username or organization name associated with your app, you will no longer be able to publish updates to your app without changing its name and branding. If you have ended up in this situation, you will receive an issue filed against your app with more details and with some options.

    Launchers

    Adding Badges, Progress Bars, and launching Actions

    Applications can show additional information in the dock as well as the application menu. This makes the application feel more integrated into the system and give user it's status at a glance. See for what you should do and what you shouldn't.

    For this integration you can use the . Since it uses the same D-Bus path as the , the API can work across many different distributions as it is widely supported by third party applications.

    hashtag
    Current API support:

    Publishing Requirements

    We have a few requirements and suggestions for publishing your app to AppCenter. For a quick intro on writing apps for elementary OS, check out our guide. For more developer-oriented tips, see the .

    hashtag
    Hard Requirements

    The following are hard requirements for all apps submitted to AppCenter. Both automated and human reviews will check these requirements before each app and app update is approved and released to users.

    Custom Resources

    Include additional resources with your app like icons or CSS files using GResource

    When using GResource, custom resource files will be compiled into your app's binary, ensuring they're available and loaded when your app launches. Regardless of which type of resource you'd like to include, you'll need to create a gresource.xml file and include it in your build system.

    Make sure to start with a Gtk.Application as described in the . In the data directory, create a new file called gresource.xml like the one below. Then update your meson build file to include steps to build the resource into your binary. Make sure to update the resource prefix to match your app's RDNN.

    AppStream MetaInfo Creatorwww.freedesktop.orgchevron-right
    OARS: Open Age Ratings Servicehughsie.github.iochevron-right
    hashtag
    Technical Requirements

    Your app must be hosted in a GitHub repositoryarrow-up-right. AppCenter Dashboard works by importing source code from a GitHub repository and building it in a clean environment. To ensure reproducible builds, transparency, and auditability, binaries cannot be uploaded or included alongside the source code to be installed on users' devices.

    Your app may be written in any language, but the front-end must be a native Gtk3 or Gtk4 app. Web, Electron, Qt, Java, and other non-native app front-ends will be rejected during the review process. A game may be excepted from this requirement so long as it uses native window decorations and is generally usable on both loDPI and HiDPI displays.

    Your app must not attempt to modify, replace, or append software sources on the target system.

    Your app must not attempt to modify other apps or system programs.

    hashtag
    Packaging

    Your app must come with a Flatpak manifestarrow-up-right. elementary OS uses Flatpak packages to manage software installation and updates. Your app cannot be downloaded without it.

    Your Flatpak manifest must use the elementary runtime. Manifests using for example the GNOME runtime will faill to build since that runtime is not available in the AppCenter remote.

    Your sandbox holes must be reasonable as decided by app reviewers. Generally you should aim for your app to be as tightly sandboxed as possible and provide an explanation for any sandbox holes your app requires. AppCenter will warn about sandbox holes when downloading your app.

    hashtag
    Extensions & Plugins

    AppCenter supports publishing apps that meet these requirements; extensions or plugins (eg. to the Panel, System Settings, or other apps) are not currently supported.

    hashtag
    Metadata Requirements

    In general, your app's metadata should not refer to "elementary" or "elementary OS" in user-facing strings—it is assumed that all apps submitted to AppCenter are designed for elementary OS, and users don't need to be reminded of this. If there is a rare, legitimate reason for mentioning elementary, it must abide by the elementary Brand Guidelinesarrow-up-right.

    hashtag
    MetaInfo and OARS

    Your app must install an metainfo.xml file to /usr/share/metainfo. AppCenter uses this metadata to create a listing for your app. It cannot be displayed in AppCenter without it.

    Your metainfo.xml file must contain a screenshot tag that references a screenshot of your app with elementary OS default settings including the GTK stylesheet, icons, window button position, etc. Screenshots referenced in your MetaInfo should not contain marketing copy, illustrations, or other elements aside from a full-window screenshot of your app in use.

    Your metainfo.xml file must contain a developerarrow-up-right tag that references your name or the name of your organization.

    Your metainfo.xml must include accurate Open Age Rating Service (OARS)arrow-up-right data. OARS uses a short, self-reported survey that only takes a few moments to output the required XML. Reviewers will check this data for accuracy in order for your app to be published.

    For more information about the metainfo.xml format see, see the appstream specificationarrow-up-right.

    hashtag
    Desktop File

    Your app must install a .desktop file to /usr/share/applications. elementary OS uses a .desktop file to display your app in the applications launcher and the dock. Without this file, your app will not be visible to users.

    Your .desktop file must not contain NoDisplay=true or anything else that prevents it from showing up in the applications launcher.

    hashtag
    Icons

    Your app must install icons to /usr/share/icons/hicolor in 32px, 48px, 64px, and 128px. These are the icon sizes that will be displayed in the Applications menu's search, grid, and category views, Multitasking View, the dock, and in AppCenter.

    hashtag
    Legal Requirements

    You must have permission to redistribute any software you attempt to publish through AppCenter. If you are not the copyright holder or the source code is not openly licensed, you likely do not have permission to redistribute the software.

    You must have permission to use any trademarks in your software or its metadata. You may not publish your app using trademarks reserved by others.

    Your app must comply with all applicable laws as well as the terms of any services your app utilizes.

    hashtag
    Other Publishing Requirements

    hashtag
    Naming

    To protect both users and developers, your app's name must not be the same as or confusingly similar to an existing app in AppCenter or another brand, as determined by app reviewers.

    Your app should not be named generically like "Web Browser" or "File Manager".

    Your app should not use "elementary", "Pantheon", or other elementary brand names in its naming scheme. It should also not be formatted with a leading lowercase "e", such as "eApp".

    hashtag
    Launching

    Your app should display its own graphical user interface on launch; it should not open another app or system component without user interaction inside your app UI. For example, if your app operates on a given file, it should display its own UI with an "Open File" button (or similar) before throwing an Open File dialog. If your app can request elevated permissions for certain actions, those permissions should be requested after your app's UI is shown and preferably only on-demand when actions requiring those permissions are performed.

    hashtag
    App Stores

    Your app cannot be an "app store," as ultimately determined by app reviewers. This includes but is not limited to apps that look and function confusingly similarly to AppCenter, provide software from other sources, link to an online app store, and/or facilitate payments for apps in the system repositories.

    hashtag
    Duplicate Apps

    Each app submission from a developer should be genuine and unique as determined by app reviewers. Submissions that are overly similar to existing apps from the same developer or are a a fork of an existing app with little to no changes may be rejected.

    hashtag
    Suggestions

    For the best experience, we strongly suggest the following before you publish your app:

    hashtag
    Use a GitHub Organization

    Once your app is submitted you cannot change the GitHub account that it is linked to. We recommend you use a GitHub organization as the owner of the repository so it can be transferred in the future if you so choose. This also makes it easier to manage community contributions.

    Note that anyone with access to the GitHub organization will be able to manage releases and monetization.

    hashtag
    Repository and App ID

    While AppCenter Dashboard supports repositories with dashes and underscores, it vastly simplifies things to omit them from your GitHub repository name and subsequently your app ID. Some components in the desktop require workarounds for ID matching with dashes and underscores, so it is typically simpler to stick to all lowercase with no dashes or underscores.

    hashtag
    Human Interface Guidelines

    Apps should generally abide by the elementary Human Interface Guidelinesarrow-up-right. Guidelines around Notificationsarrow-up-right may be strictly enforced, as it is important that apps behave as expected by users.

    hashtag
    Tips for Games

    If your game UI properly scales when the window is resized, one way to support both HiDPI and loDPI (even if the engine doesn't out of the box) is to launch your game maximized. That way the UI will scale up no matter the resolution or scaling factor of the display.

    Getting Started
    Tips series on our blogarrow-up-right
    https://github.com/elementary/appcenter-reviews/new/main/applicationsgithub.comchevron-right
    Select this link to automatically open a new file in the correct location
    The number of columns the widget should span.
  • The number of rows the widget should span.

  • We added multiple widgets into a single Gtk.Grid using the attach method to create complex layouts containing Buttons and Labels.
    orientationarrow-up-right
    all four sidesarrow-up-right
    GLib.Settings.bind () on Valadocarrow-up-right
    here on GitHubarrow-up-right
    tags can remain in your MetaInfo file forever, and we encourage it; AppCenter may use them to show changes across multiple releases of your app
    Release Tag Specificationarrow-up-right
    Service
    Badge Counter
    Progress Bar
    Actions

    Applications Menu

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    Dock

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    hashtag
    Setting Up

    Before writing any code, you must add the library Granitearrow-up-right to your build system. We already installed this library during The Basic Setup when we installed elementary-sdk. Open your meson.build file and add the new dependency to the executable method.

    Your app must also be a Gtk.Application with a correctly set application_id as we previously set up in Hello World.

    Though we haven't made any changes to our source code yet, change into your build directory and run ninja to build your project. It should still build without any errors. If you do encounter errors, double check your changes and resolve them before continuing.

    Once you've set up granite in your build system and created a new Gtk.Application with an application_id, it's time to write some code.

    hashtag
    Badges

    Showing a badge in the dock and Applications Menu with the number 12 can be done with the following lines:

    Keep in mind you have to set the set_badge_visible property to true, and use an int64 type for the set_badge property. The suffix .begin is required here since these are asynchronous methods.

    hashtag
    Progress Bars

    The same goes for showing a progress bar, here we show a progress bar showing 20% progress:

    As you can see, the method set_progress takes a double value type and is a range between 0 and 1: from 0% to 100%. As with badges, Don't forget that set_progress_visible must be true and .begin is required for asynchronous methods.

    hashtag
    Actions

    Actions are specific functions your app can perform without already being open; think of them as alternate and more specific entry points into your app. Actions appear in the context menu of your app icon in the Applications Menu and Dock, and are searchable by name from the Applications Menu.

    hashtag
    D-Bus activation

    Your app needs to support D-Bus activation in order to use actions as entry points. This does not require any changes to the application source code. All that is needed is a service file which is not unlike the .desktop file that you are already familiar with. Create a new .service file in the data directory:

    To install the service add the following to your meson.build file:

    Lastly, update the .desktop file by adding the DBusActivatable line to the Desktop Entry group:

    hashtag
    Declaring actions

    You can use any action defined in the app namespace, i.e. registered with GLib.Application, as an entry point for your application. Implementing actions is covered in-depth in the actions sectionarrow-up-right. They must also be declared in a new Actions line in your app's .desktop file. This line should contain a ; separated list of action names:

    Then use a dedicated group, named after the unique action name, to define the details of each action:

    circle-exclamation

    The action name used in .desktop file, both in Desktop Entry and later in Desktop Action groups, needs to match exactly the name used to register the action with GLib.Application in the source code.

    The Icon line is optional and should be an icon which represents the action that will be performed. The Exec line should be specified, but is used only for backwards compatibility in case your app ever runs in an environment without D-Bus activation support.

    The action name should not include your app's name, as it will always be displayed alongside your app. The action icon should also not be your app icon, as it may be shown in the menu for your app icon, or badged on top of the app icon.

    See the freedesktop.org Additional applications actions sectionarrow-up-right for a detailed description of what keys are supported and what they do.

    circle-info

    If you're having trouble, you can view the full example code here on GitHubarrow-up-right.

    HIG for Dock integrationarrow-up-right
    Granite.Services.Application APIarrow-up-right
    Unity Launcher APIarrow-up-right

    Now that we have a gresource.xml file and have included it in the build system, we can add files and reference them in our app.

    hashtag
    Icons

    You can provide custom icons and have your app automatically refer to them by name and choose the correct size by adding them your Gresource file under the namespace /icons. For this to work properly, your resource path must match your application's ID.

    Add a custom icon to the data directory, and then update your gresource.xml file to reference it:

    If you want to use the same icon name in multiple sizes in your app or it is a symbolic icon, you must alias the icon to paths in hicolorarrow-up-right and GTK will automatically load the correct version when its size is referenced:

    circle-info

    When creating icons, it is important to know which sizes will be used and to design and hint the icon at that size. For more information about creating and hinting icons, check out the Human Interface Guidelinesarrow-up-right.

    The last step is to create a Gtk.Image or Gtk.Button using your custom icon and add it to the main window:

    previous section
    # Include the GNOME module
    gnome = import('gnome')
    
    # Tell meson where to find our resources file and to compile it as a GResource
    gresource = gnome.compile_resources(
        'gresource',
        'data' / 'gresource.xml',
        source_dir: 'data'
    )
    
    # Add the gresource to the executable step to be build into the app binary
    executable(
        meson.project_name(),
        gresource,
        'src' / 'Application.vala',
        dependencies: [
            dependency('gtk4')
        ],
        install: true
    )
    <?xml version="1.0" encoding="UTF-8"?>
    <gresources>
      <gresource prefix="io/github/myteam/myapp">
      </gresource>
    </gresources>
    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
    box.append (new Gtk.Label ("Label 1"));
    box.append (new Gtk.Label ("Label 2"));
    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
    box.append (new Gtk.Label ("Label 1"));
    box.append (new Gtk.Label ("Label 2"));
    
    var main_window = new Gtk.ApplicationWindow (this) {
        child = box
    };
    // First we create all the widgets we want to lay out in our grid
    var hello_button = new Gtk.Button.with_label ("Hello");
    var hello_label = new Gtk.Label ("Label 1");
    
    var goodbye_button = new Gtk.Button.with_label ("Goodbye");
    var goodbye_label = new Gtk.Label ("Label 2");
    
    // Then we create a new Grid and set its spacing properties
    var grid = new Gtk.Grid () {
        column_spacing = 6,
        row_spacing = 6
    };
    
    // Finally, we attach those widgets to the Grid. Attach first row of widgets
    grid.attach (hello_button, 0, 0, 1, 1);
    grid.attach (hello_label, 1, 0, 1, 1);
    
    // Attach second row of widgets
    grid.attach (goodbye_button, 0, 1);
    grid.attach_next_to (goodbye_label, goodbye_button, Gtk.PositionType.RIGHT, 1, 1);
    
    var main_window = new Gtk.ApplicationWindow (this) {
        child = grid
    };
    <?xml version="1.0" encoding="UTF-8"?>
    <schemalist>
      <schema path="/io/github/yourusername/yourrepositoryname/" id="io.github.yourusername.yourrepositoryname">
        <key name="useless-setting" type="b">
          <default>false</default>
          <summary>Useless Setting</summary>
          <description>Whether the switch is toggled</description>
        </key>
      </schema>
    </schemalist>
    protected override void activate () {
        var useless_switch = new Gtk.Switch () {
            halign = CENTER,
            valign = CENTER
        };
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            child = useless_switch
        };
        main_window.present ();
    
        var settings = new Settings ("io.github.yourusername.yourrepositoryname");
        settings.bind ("useless-setting", useless_switch, "active", DEFAULT);
    }
    data/meson.build
    install_data (
        'gschema.xml',
        install_dir: get_option('datadir') / 'glib-2.0' / 'schemas',
        rename: meson.project_name() + '.gschema.xml'
    )
    gnome = import('gnome')
    gnome.post_install(glib_compile_schemas: true)
    
    subdir('data')
    <release version="1.1.0 date="2023-05-21">
      <description>
        <p>Now with support for WebP images and an option to maintain aspect ratio when cropping! Plus, the main view now loads twice as fast.</p>
        <p>Other improvements:</p>
        <ul>
          <li>🇫🇷️ Updated French translations</li>
          <li>⚙️ Cleaner code with improved stability</li>
        </ul>
      </description>
    </release>
    <release version="1.1.0 date="2023-05-21">
      <description>
        <p>Fix bug #1234; rewrote method to be async</p>
        <p>Linted all the code, removed trailing whitespace</p>
        <p>Refactored utils</p>
        <p>Updated VAPIs</p>
      </description>
    </release>
    <release version="1.1.0 date="2023-05-21">
      <description>
        <p>Now with support for WebP images and an option to maintain aspect ratio when cropping! Plus, the main view now loads twice as fast.</p>
      </description>
      <issues>
        <issue url="https://github.com/myteam/myapp/issues/28">Crash when opening corrupt JPEG</issue>
        <issue url="https://github.com/myteam/myapp/issues/31">Image gets pixelated after rotating</issue>
      </issues>
    </release>
    executable(
        meson.project_name(),
        'src/Application.vala',
        dependencies: [
            dependency('granite-7'),
            dependency('gtk4')
        ],
        install: true
    )
    Granite.Services.Application.set_badge_visible.begin (true);
    Granite.Services.Application.set_badge.begin (12);
    Granite.Services.Application.set_progress_visible.begin (true);
    Granite.Services.Application.set_progress.begin (0.2f);
    myapp.service
    [D-BUS Service]
    Name=io.github.myteam.myapp
    Exec=io.github.myteam.myapp --gapplication-service
    # Install D-Bus service, so that application can be started by D-Bus
    install_data(
        'data' / 'myapp.service',
        install_dir: get_option('datadir') / 'dbus-1' / 'services',
        rename: meson.project_name() + '.service',
    )
    [Desktop Entry]
    Version=1.0
    Type=Application
    Name=MyApp
    [...]
    DBusActivatable=true
    [Desktop Entry]
    Version=1.0
    Type=Application
    Name=MyApp
    [...]
    Actions=my-action;
    [Desktop Action my-action]
    Name=My Great Action
    Icon=io.github.myteam.myapp.my-action-icon
    Exec=io.github.myteam.myapp
    <?xml version="1.0" encoding="UTF-8"?>
    <gresources>
      <gresource prefix="io/github/myteam/myapp/icons">
        <file compressed="true" preprocess="xml-stripblanks">custom-icon.svg</file>
      </gresource>
    </gresources>
    <?xml version="1.0" encoding="UTF-8"?>
    <gresources>
      <gresource prefix="io/github/myteam/myapp/icons">
        <file alias="24x24/actions/custom-icon.svg" compressed="true" preprocess="xml-stripblanks">custom-icon-24.svg</file>
        <file alias="24x24@2/actions/custom-icon.svg" compressed="true" preprocess="xml-stripblanks">custom-icon-24.svg</file>
        <file alias="32x32/actions/custom-icon.svg" compressed="true" preprocess="xml-stripblanks">custom-icon-32.svg</file>
        <file alias="32x32@2/actions/custom-icon.svg" compressed="true" preprocess="xml-stripblanks">custom-icon-32.svg</file>
        <!--Icons under the -symbolic namespace will be automatically recolored to match text color-->
        <file alias="scalable/actions/custom-icon-symbolic.svg" compressed="true" preprocess="xml-stripblanks">custom-icon-symbolic.svg</file>
      </gresource>
    </gresources>
    protected override void activate () {
        // This will create an image with an icon size of 32px
        var image = new Gtk.Image.from_icon_name ("custom-icon") {
            pixel_size = 32
        };
    
        // This will create a button with an icon size of 16px
        var button = new Gtk.Button.from_icon_name ("custom-icon");
    
        var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12);
        box.append (image);
        box.append (button);
    
        var main_window = new Gtk.ApplicationWindow (this) {
            child = box
        };
        main_window.present ();
    }

    Panes

    Creating multi-pane app layouts

    So far, you've created apps with a single HeaderBar, but what about creating an app with a multi-pane layout like Mail or Tasks?

    Before we begin, make sure you've included Granite in the build dependencies of your "meson.build" file. We'll be using it later for a couple of style classes.

    Start with a new Gtk.Application, but this time set the titlebar property of the ApplicationWindow to an invisible grid. This will replace the default HeaderBar of the window since we'll be creating our own HeaderBars inside the paned layout.

    var main_window = new Gtk.ApplicationWindow (this) {
        titlebar = new Gtk.Grid () { visible = false }
    };

    Then create a new Gtk.Panedarrow-up-right and set that as the child of the ApplicationWindow. A Paned is a resizable widget that can contain two children. There are various properties you can set to control the widgets behavior when the AppliactionWindow is resized; For the purposes of this tutorial we want the first pane to stay a fixed size and we don't want someone to be able to make the window smaller than the contents of both sides of the Paned.

    Next, let's create a new Gtk.HeaderBar and hide its title buttons. Then, we're going to pack our own that only contain the buttons for one side of the Window. And finally, we'll put that HeaderBar in a Box and set that as the start_child of the Paned.

    Do the same thing again for the end_child of the Paned: create a HeaderBar with END WindowControls, inside a vertical Box.

    Finally, make a few small tweaks:

    1. Set an empty label as the title_widget in the start header

    2. Add Granite.STYLE_CLASS_VIEW to the end box

    3. Set the default size and title properties of the ApplicationWindow

    The final result should look like this:

    Logo

    Actions

    Creating tool items, GLib.Actions, and keyboard shortcuts

    GTK and GLib have a powerful API called which can be used to define the primary actions of your app, assign them keyboard shortcuts, use them as entry points for your app and tie them to widgets like Buttons and Menu Items. In this section, we're going to create a Quit action for your app with an assigned keyboard shortcut and a Button that shows that shortcut in a tooltip.

    hashtag
    Gtk.HeaderBar

    Begin by creating a Gtk.Application

    meson.build
    executable(
        meson.project_name(),
        'src' / 'Application.vala',
        dependencies: [
            dependency('granite-7'),
            dependency('gtk4')
        ],
        install: true
    )
    Gtk.WindowControlsarrow-up-right
    var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) {
        resize_start_child = false,
        shrink_end_child = false,
        shrink_start_child = false
    };
    
    var main_window = new Gtk.ApplicationWindow (this) {
        child = paned,
        titlebar = new Gtk.Grid () { visible = false }
    };
    var start_header = new Gtk.HeaderBar () {
        show_title_buttons = false
    };
    start_header.add_css_class (Granite.STYLE_CLASS_FLAT);
    start_header.pack_start (new Gtk.WindowControls (Gtk.PackType.START));
    
    var start_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
    start_box.append (start_header);
    
    var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) {
        start_child = start_box,
        resize_start_child = false,
        shrink_end_child = false,
        shrink_start_child = false
    };
    
    var main_window = new Gtk.ApplicationWindow (this) {
        child = paned,
        titlebar = new Gtk.Grid () { visible = false }
    };
    var start_header = new Gtk.HeaderBar () {
        show_title_buttons = false
    };
    start_header.add_css_class (Granite.STYLE_CLASS_FLAT);
    start_header.pack_start (new Gtk.WindowControls (Gtk.PackType.START));
    
    var start_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
    start_box.append (start_header);
    
    var end_header = new Gtk.HeaderBar () {
        show_title_buttons = false
    };
    end_header.add_css_class (Granite.STYLE_CLASS_FLAT);
    end_header.pack_end (new Gtk.WindowControls (Gtk.PackType.END));
    
    var end_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
    end_box.append (end_header);
    
    var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) {
        start_child = start_box,
        end_child = end_box,
        resize_start_child = false,
        shrink_end_child = false,
        shrink_start_child = false
    };
    
    var main_window = new Gtk.ApplicationWindow (this) {
        child = paned,
        titlebar = new Gtk.Grid () { visible = false }
    };
    Application.vala
    public class MyApp : Gtk.Application {
        public MyApp () {
            Object (
                application_id: "io.github.myteam.myapp",
                flags: ApplicationFlags.DEFAULT_FLAGS
            );
        }
    
        protected override void activate () {
            var start_header = new Gtk.HeaderBar () {
                show_title_buttons = false,
                title_widget = new Gtk.Label ("")
            };
            start_header.add_css_class (Granite.STYLE_CLASS_FLAT);
            start_header.pack_start (new Gtk.WindowControls (Gtk.PackType.START));
    
            var start_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
            start_box.append (start_header);
    
            var end_header = new Gtk.HeaderBar () {
                show_title_buttons = false
            };
            end_header.add_css_class (Granite.STYLE_CLASS_FLAT);
            end_header.pack_end (new Gtk.WindowControls (Gtk.PackType.END));
    
            var end_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
            end_box.add_css_class (Granite.STYLE_CLASS_VIEW);
            end_box.append (end_header);
    
            var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) {
                start_child = start_box,
                end_child = end_box,
                resize_start_child = false,
                shrink_end_child = false,
                shrink_start_child = false
            };
    
            var main_window = new Gtk.ApplicationWindow (this) {
                child = paned,
                default_height = 300,
                default_width = 300,
                titlebar = new Gtk.Grid () { visible = false },
                title = "Hello World"
            };
            main_window.present ();
        }
    
        public static int main (string[] args) {
            return new MyApp ().run (args);
        }
    }
    with a
    Gtk.ApplicationWindow
    as you've done in
    . Once you have that set up, let's create a new
    . Typically your app will have a HeaderBar, at the top of the window, which will contain tool items that users will interact with to trigger your app's actions.

    Since we're using this HeaderBar as our app's main titlebar, we need to set show_title_buttons to true so that GTK knows to include window controls. We can then override our Window's built-in titlebar with the titlebar property.

    Now, create a new Gtk.Buttonarrow-up-right with a big colorful icon and add it to the HeaderBar:

    Build and run your app. You can see that it now has a custom HeaderBar with a big red icon in it. But when you click on it, nothing happens.

    circle-info

    elementary OS ships with a large set of system icons that you can use in your app for actions, status, and more. You can browse the full set of named icons using the Icon Browserarrow-up-right app, available in AppCenter.

    hashtag
    GLib.SimpleAction

    Define a new Quit action and register it with Application from inside the startup method:

    You'll notice that we do a few things here:

    • Instantiate a new GLib.SimpleActionarrow-up-right with the name "quit"

    • Add the action to our Gtk.Application's ActionMaparrow-up-right

    • Set the "accelerators" (keyboard shortcuts) for "app.quit" to <Control>q and <Control>w. Notice that the action name is prefixed with app; this refers to the namespace of the ActionMap built in to Gtk.Application

    • Connect the activate signal of our SimpleAction to Application's function.

    Now we can tie the action to the HeaderBar Button by assigning the action_name property of our Button:

    Build and run your app again and see that you can now quit the app either through the defined keyboard shortcuts or by clicking the Button in the HeaderBar.

    circle-info

    Accelerator strings follow a format defined by Gtk.accelerator_parsearrow-up-right. You can find a list of key values on Valadocarrow-up-right

    circle-info

    Actions defined like this, and registered with Application, can be used as entry points into the app. You can find out more about this integration in the launchers section.

    hashtag
    Granite.markup_accel_tooltip

    You may have noticed that in elementary apps you can hover your pointer over tool items to see a description of the button and any available keyboard shortcuts associated with it. We can add the same thing to our Button with Granite.markup_accel_tooltip ()arrow-up-right.

    First, make sure you've included Granite in the build dependencies declared in your meson.build file, then set the tooltip_markup property of your HeaderBar Button:

    Build and run your app and then hover over the HeaderBar Button to see its description and associated keyboard shortcuts.

    circle-info

    If you're having trouble, you can view the full example code here on GitHubarrow-up-right. You can also learn more from GLib.Action reference documentationarrow-up-right.

    GLib.Actionarrow-up-right
    Actionablearrow-up-right
    A Gtk.Application with a HeaderBar containing a large image button
    previous examples
    Gtk.HeaderBararrow-up-right
    executable(
        meson.project_name(),
        'src' / 'Application.vala',
        dependencies: [
            dependency('granite-7'),
            dependency('gtk4')
        ],
        install: true
    )
    var button = new Gtk.Button.from_icon_name ("process-stop") {
        action_name = "app.quit",
        tooltip_markup = Granite.markup_accel_tooltip (
            get_accels_for_action ("app.quit"),
            "Quit"
        )
    };
    Application.vala
    protected override void activate () {
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "Actions",
            titlebar = headerbar
        };
        main_window.present ();
    }
    Application.vala
    protected override void activate () {
        var button = new Gtk.Button.from_icon_name ("process-stop");
        button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS);
    
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
        headerbar.pack_start (button);
        
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "Actions",
            titlebar = headerbar
        };
        main_window.present ();
    }
    Application.vala
    protected override void startup () {
        base.startup ();
    
        var quit_action = new SimpleAction ("quit", null);
    
        add_action (quit_action);
        set_accels_for_action ("app.quit",  {"<Control>q", "<Control>w"});
        quit_action.activate.connect (quit);
    }
        var button = new Gtk.Button.from_icon_name ("process-stop") {
            action_name = "app.quit"
        };
    quit ()arrow-up-right

    Popovers

    Create menus and popovers manually or automatically using a menu model

    circle-info

    Before beginning this section, create a new Gtk.Application complete with a desktop launcher file, packaging, etc and add a Quit action as covered in the Actions section.

    hashtag
    Gtk.Popover

    Popovers are a very flexible container widget that can contain any other widgets, just like your main window. In this section we'll be opening a popover by clicking on a .

    Create a new MenuButton and add it to the end of the HeaderBar. Use the icon-name property to give it an icon, and add Granite.STYLE_CLASS_LARGE_ICONS to make it bigger just like we did in the section with our image Button. You can also set the primary property to true which will make this menu open when pressing the keyboard shortcut F10. Finally, add a tooltip to make the keyboard shortcut more discoverable.

    Now create a new Button with the action app.quit, but instead of giving it a label, set a as its child; this will allow us to show the associated keyboard shortcut for the menu item. Finally, add Granite.STYLE_CLASS_MENUITEM so that the button shows as a borderless menu item.

    Next create a Popover and set the quit Button as its child. Adding Granite.STYLE_CLASS_MENU will automatically set up some margins and sizing for us. Finally, set the popover property of the MenuButton to point to our Popover.

    Build and run your app. Notice that you can open and close the Popover automatically by activating the MenuButton with your pointer or by using the keyboard shortcut F10. The Popover is styled correctly as a menu and clicking the quit Button closes your app.

    hashtag
    GLib.Menu

    You can also create Popover menus automatically from Actions. In this section we'll be creating a with a and opening it with a secondary click.

    Start by creating a new Menu and add an item "Quit" with the action name "app.quit". Then create a new PopoverMenu with the Menu we just created as its model. We can position and halign the PopoverMenu so that it appears where we expect in relation to the pointer, and we'll set has_arrow to false, since this menu won't be pointing to a button.

    Now let's add a new and set Gdk.BUTTON_SECONDARY as the button it responds to. Then at the end of the activate () function, we'll connect to the release () signal of that gesture.

    When the GestureClick is released, it tells us the coordinates of where that gesture took place in our app, which we'll store in a . Then we can set the pointing_to property of our popover to that rectangle, and open it with the popup () function.

    There's just one more thing to do for our secondary click menu, and that's to add a widget that will be the parent of our PopoverMenu and receive the GestureClick. Add a Box to the main window, connect the GestureClick using add_controller (), and parent the PopoverMenu with set_parent ().

    Build and run your app, then use a secondary click to open the PopoverMenu at your pointer. You'll notice that the keyboard shortcut is shown automatically in the menu, everything is styled correctly without having to manually add any style classes, and clicking the menu item closes your app.

    circle-info

    If you're having trouble, you can view the full example code . You can also learn more from Gtk.Popover .

    Notifications

    Sending Notification Bubbles with GLib.Notification

    By now you've probably already seen the notification bubbles that appear on the top right of the screen. Notifications are a way to provide updates about the state of your app. For example, they can inform you that a long running background process has been completed or a new message has arrived. In this section we are going to show you just how to get them to work in your app.

    hashtag
    Making Preparations

    Create a new Gtk.Application

    Gtk.MenuButtonarrow-up-right
    Actions
    Granite.AccelLabelarrow-up-right
    Gtk.PopoverMenuarrow-up-right
    GLib.Menuarrow-up-right
    Gtk.GestureClickarrow-up-right
    Gdk.Rectanglearrow-up-right
    here on GitHubarrow-up-right
    reference documentationarrow-up-right
    Application.vala
    protected override void activate () {
        var menu_button = new Gtk.MenuButton () {
            icon_name = "open-menu",
            primary = true,
            tooltip_markup = Granite.markup_accel_tooltip ({"F10"}, "Menu")
        };
        menu_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS);
    
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
        headerbar.pack_end (menu_button);
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
    }
    Application.vala
    protected override void activate () {
        var quit_button = new Gtk.Button () {
            action_name = "app.quit",
            child = new Granite.AccelLabel.from_action_name ("Quit", "app.quit")
        };
        quit_button.add_css_class (Granite.STYLE_CLASS_MENUITEM);
    
        var popover = new Gtk.Popover () {
            child = quit_button
        };
        popover.add_css_class (Granite.STYLE_CLASS_MENU);
    
        var menu_button = new Gtk.MenuButton () {
            icon_name = "open-menu",
            popover = popover,
            primary = true
        };
        menu_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS);
    
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
        headerbar.pack_end (menu_button);
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
    }
    Application.vala
    protected override void activate () {
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        var menu = new Menu ();
        menu.append ("Quit", "app.quit");
    
        var popover_menu = new Gtk.PopoverMenu.from_model (menu) {
            halign = Gtk.Align.START,
            has_arrow = false,
            position = Gtk.PositionType.BOTTOM
        };
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
    }
    Application.vala
    protected override void activate () {
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        var menu = new Menu ();
        menu.append ("Quit", "app.quit");
    
        var popover_menu = new Gtk.PopoverMenu.from_model (menu) {
            halign = Gtk.Align.START,
            has_arrow = false,
            position = Gtk.PositionType.BOTTOM
        };
    
        var secondary_click_gesture = new Gtk.GestureClick () {
            button = Gdk.BUTTON_SECONDARY
        };
    
        var main_window = new Gtk.ApplicationWindow (this) {
            default_height = 300,
            default_width = 300,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
    
        secondary_click_gesture.released.connect ((n_press, x, y) => {
            var rect = Gdk.Rectangle () {
                x = (int) x,
                y = (int) y
            };
    
            popover_menu.pointing_to = rect;
            popover_menu.popup ();
        });
    }
    Application.vala
    protected override void activate () {
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        var menu = new Menu ();
        menu.append ("Quit", "app.quit");
    
        var popover_menu = new Gtk.PopoverMenu.from_model (menu) {
            halign = Gtk.Align.START,
            has_arrow = false,
            position = Gtk.PositionType.BOTTOM
        };
    
        var secondary_click_gesture = new Gtk.GestureClick () {
            button = Gdk.BUTTON_SECONDARY
        };
    
        var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
        box.add_controller (secondary_click_gesture);
        popover_menu.set_parent (box);
    
        var main_window = new Gtk.ApplicationWindow (this) {
            child = box,
            default_height = 300,
            default_width = 300,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
    
        secondary_click_gesture.released.connect ((n_press, x, y) => {
            var rect = Gdk.Rectangle () {
                x = (int) x,
                y = (int) y
            };
    
            popover_menu.pointing_to = rect;
            popover_menu.popup ();
        });
    }
    complete with a desktop launcher file, packaging, etc. You can review this in
    .

    In your .desktop file, add the line X-GNOME-UsesNotifications=true to the end of the file. This is what will make your app appear in System Settings so that notification preferences can be set.

    hashtag
    Sending Notifications

    In your Application.vala file, in the activate () function, create a new Gtk.Buttonarrow-up-right and add it to a Gtk.Boxarrow-up-right with some margins. Then set that box as the child widget for your app's main window.

    Finally, connect to the clicked ()arrow-up-right signal of that button, and create a new Notification with body text, and then send it with send_notification ()arrow-up-right.

    Now build and run your app, and click the "Notify" button. Congratulations, you've learned how to send notifications!

    hashtag
    Badge Icons

    Notification with a badged icon

    Notifications will automatically contain your app's icon, but you can add additional context by setting a badge icon. Right after the line containing var notification = New Notification, add:

    Build and run your app again, and press the "Notify" button. As you can see, the notification now has a smaller badged icon placed over your app's icon. Using this method, you can set the icon to any of the named icons shipped with elementary OS.

    circle-info

    You can browse the full set of named icons using the Icon Browserarrow-up-right app, available in AppCenter.

    hashtag
    Buttons

    Notification with an action button

    You can also add buttons to notifications that will trigger actions defined in the app namespace. To add a button, first define an action in your Application class as we did in the actions section.

    Now, we can add a button to the notification with a label and the action ID.

    Build and run your app again, and press the "Notify" button. Notice that the notification now has a button with the label "Quit" and clicking it will close your app.

    circle-info

    Remember that SimpleActions added in the Application class with add_action () are automatically added in the app namespace. Notifications can't trigger actions defined in other namespaces like win.

    hashtag
    Default Action

    You may have noticed that when you click on a new notification, a new window pops up. This is happening because the notification has a default action that is executed when the user clicks on it. If you don't set it, it will activate your application, where we create a new window and present it.

    You can change the default action using set_default_action ()arrow-up-right. The default action has to be in the app namespace. If you are unsure what that means, see the actions section.

    If you want to avoid creating a new window every time your application is activated, you need to check if there is a window in Gtk.Application.active_windowarrow-up-right and present it instead.

    hashtag
    Priority

    Notifications also have priority. When a notification is set as URGENT it will stay on the screen until either you interact with it, or your application withdraws it. To make an urgent notification, use the set_priority ()arrow-up-right function

    URGENT notifications should really only be used on the most extreme cases. There are also other notification prioritiesarrow-up-right.

    hashtag
    Replace

    We now know how to send a notification, but what if you need to update it with new information? Thanks to the id argument of the send_notification ()arrow-up-right function, we can replace a notification with a matching ID. This ID can be anything you like.

    Make a new button with the label "Replace" that sends a new notification, this time with an ID. This button will replace a notification with a matching ID when clicked, instead of sending a new notification.

    Build and run your app again. Click on the buttons, first on "Notify", then "Replace". See how the "Notify" button sends a new notification each time it's clicked, but the "Replace" button replaces the contents of the same notification when it's clicked.

    hashtag
    Review

    Let's review what all we've learned:

    • We built an app that sends notifications.

    • Notifications automatically get our app's icon, but we can also add a badge icon

    • We can add buttons that trigger actions in the app namespace

    • Notifications can have a which affects their behavior

    • We can by setting a replaces ID

    circle-info

    If you're having trouble, you can view the full example code here on GitHub.arrow-up-right You can learn more from GLib.Notification reference documentationarrow-up-right.

    A notification bubble
    Our First App

    Code Style

    Recommendations for clean code bases

    Internally, elementary uses the following code style guide to ensure that code is consistently formatted both internally and across projects. Consistent and easily-legible code makes it easier for newcomers to learn and contribute. We'd like to recommend that in the spirit of Open Source collaboration, all Vala apps written in the wider ecosystem also follow these guidelines.

    hashtag
    Whitespace

    White space comes before opening parentheses or braces:

    An exception is admitted for Gettext-localized strings, where no space should go between the underscore and the opening parenthese:

    myapp.desktop
    [Desktop Entry]
    Version=1.0
    Type=Application
    
    [...]
    
    X-GNOME-UsesNotifications=true
    Application.vala
    protected override void activate () {
        var notify_button = new Gtk.Button.with_label ("Notify");
    
        var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) {
            margin_top = 12,
            margin_end = 12,
            margin_bottom = 12,
            margin_start = 12
        };
        box.append (notify_button);
    
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        var main_window = new Gtk.ApplicationWindow (this) {
            child = box,
            title = "MyApp",
            titlebar = headerbar
        };
        main_window.present ();
        
        notify_button.clicked.connect (() => {
            var notification = new Notification ("Hello World");
            notification.set_body ("This is my first notification!");
    
            send_notification (null, notification);
        });
    }
    notify_button.clicked.connect (() => {
        var notification = new Notification ("Hello World");
        notification.set_body ("This is my first notification!");
        notification.set_icon (new ThemedIcon ("process-completed"));
    
        send_notification (null, notification);
    });
    Application.vala
    public override void startup () {
        base.startup ();
    
        var quit_action = new SimpleAction ("quit", null);
    
        add_action (quit_action);
        quit_action.activate.connect (quit);
    }
    notify_button.clicked.connect (() => {
        var notification = new Notification ("Hello World");
        notification.set_body ("This is my first notification!");
        notification.add_button ("Quit", "app.quit");
    
        send_notification (null, notification);
    });
    notify_button.clicked.connect (() => {
        var notification = new Notification ("Hello World");
        notification.set_body ("This is my first notification!");
        notification.set_default_action ("app.quit");
    
        send_notification (null, notification);
    });
    Application.vala
    protected override void activate () {
        if (active_window != null) {
            active_window.present ();
            return;
        }
    
        var notify_button = new Gtk.Button.with_label ("Notify");
    
        var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) {
            margin_top = 12,
            margin_end = 12,
            margin_bottom = 12,
            margin_start = 12
        };
        box.append (notify_button);
    
        var headerbar = new Gtk.HeaderBar () {
            show_title_buttons = true
        };
    
        main_window = new Gtk.ApplicationWindow (this) {
            child = box,
            title = "MyApp",
            titlebar = headerbar
        };
        
        notify_button.clicked.connect (() => {
            var notification = new Notification ("Hello World");
            notification.set_body ("This is my first notification!");
    
            send_notification (null, notification);
        });
    
        main_window.present ();
    }
    notify_button.clicked.connect (() => {
        var notification = new Notification ("Hello World");
        notification.set_body ("This is my first notification!");
        notification.set_priority (NotificationPriority.URGENT);
    
        send_notification (null, notification);
    });
    protected override void activate () {
        var notify_button = new Gtk.Button.with_label ("Notify");
    
        var replace_button = new Gtk.Button.with_label ("Replace");
    
        var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) {
            margin_top = 12,
            margin_end = 12,
            margin_bottom = 12,
            margin_start = 12
        };
        box.append (notify_button);
        box.append (replace_button);
    
        [...]
    
        replace_button.clicked.connect (() => {
            var notification = new Notification ("Hello Again");
            notification.set_body ("This is my second Notification!");
    
            send_notification ("update", notification);
        });
    }
    priority
    replace outdated notifications
    A new Gtk.Application with a button that sends notifications

    Whitespace goes between numbers and operators in all math-related code.

    Lines consisting of closing brackets (} or )) should be followed by an empty line, except when followed by another closing bracket or an else statement.

    hashtag
    Indentation

    hashtag
    Vala

    Vala code is indented using 4 spaces for consistency and readability.

    In classes, functions, loops and general flow control, the first brace is on the end of the first line ("One True Brace Style"), followed by the indented code, and a line closing the function with a brace:

    On conditionals and loops, always use braces even if there's only one line of code:

    Cuddled else and else if:

    If you are checking the same variable more than twice, use switch/case instead of multiple else/if:

    hashtag
    Markup

    Markup languages like HTML, XML, and YML should use two-space indentation since they are much more verbose and likely to hit line-length issues sooner.

    hashtag
    Classes and Files

    A file should only contain one public class.

    All files have the same name as the class in them. For example, a file containing the class AbstractAppGrid should be called "AbstractAppGrid.vala"

    Classes should be named in a descriptive way, and include the name of any parent classes. For example, a class that subclasses Gtk.ListBoxRow and displays the names of contacts should be called ContactRow.

    hashtag
    Comments

    Comments are either on the same line as the code they reference or in a special line.

    Comments are indented alongside the code, and obvious comments do more harm than good.

    Sometimes detailed descriptions in the context of translatable strings are necessary for disambiguation or to help in the creation of accurate translations. For these situations use /// TRANSLATORS: comments.

    hashtag
    Variable, Class, and Function Names

    Variable and function names are all lower case and separated by underscores:

    Classes and enums are Upper Camel Case (aka Pascal Case):

    Constants and enum members should be all uppercase and separated by underscores:

    The values of constant strings (such as when used with GLib.Action) should be lowercase and separated with dashes:

    hashtag
    Casting

    Avoid using as keyword when casting as it might give null as result, which could be forgotten to check.

    hashtag
    Prefer Properties Over Get/Set Methods

    In places or operations where you would otherwise use get or set , you should make use of = instead.

    For example, instead of using

    you should use

    hashtag
    Initialize Objects with Properites

    This is especially clearer when initializing an object with many properties. Avoid the following

    and instead do this

    hashtag
    Create Classes with Properties

    This goes for creating methods inside of classes as well. Instead of

    you should use

    or, where you need some extra logic in the getters and setters:

    Preferring properties in classes enables the use of GLib.Object.bind_property ()arrow-up-right between classes instead of needing to create signals and handle changing properties manually.

    hashtag
    Vala Namespaces

    Referring to GLib is not necessary. If you want to print something instead of:

    You can use

    hashtag
    String Formatting

    Avoid using literals when formatting strings:

    Instead, prefer printf style placeholders:

    Warnings in Vala use printf syntax by default:

    hashtag
    GTK events

    Gtk widgets are intended to respond to click events that can be described as "press + release" instead of "press". Usually it is better to respond to toggle and release events instead of press.

    hashtag
    Columns Per Line

    Ideally, lines should have no more than 80 characters per line, because this is the default terminal size. However, as an exception, more characters could be added, because most people have wide-enough monitors nowadays. The hard limit is 120 characters.

    hashtag
    Splitting Arguments Into Lines

    For methods that take multiple arguments, it is not uncommon to have very long line lengths. In this case, treat parenthesis like brackets and split lines at commas like so:

    hashtag
    EditorConfig

    If you are using elementary Code or your code editor supports EditorConfigarrow-up-right, you can use this as a default .editorconfig file in your projects:

    public string get_text () {}
    if (a == 5) {
        return 4;
    }
    
    for (i = 0; i < maximum; i++) {}
    my_function_name ();
    var my_instance = new Object ();
    // Space before parentheses since it's normal method:
    button.label = set_label_string ();
    // No space before parentheses since it's gettext-localized string:
    button.tooltip_text = _("Download");
    c = (n * 2) + 4;
    if (condition) {
        // ...
    } else {
        // ...
    }
    
    // other code
    public int my_function (int a, string b, long c, int d, int e) {
        if (a == 5) {
            b = 3;
            c += 2;
            return d;
        }
    
        return e;
    }
    if (my_var > 2) {
        print ("hello\n");
    }
    if (a == 4) {
        b = 1;
        print ("Yay");
    } else if (a == 3) {
        b = 3;
        print ("Not so good");
    }
    switch (week_day) {
       case "Monday":
           message ("Let's work!");
           break;
       case "Tuesday":
       case "Wednesday":
           message ("What about watching a movie?");
           break;
       default:
           message ("You don't have any recommendation.");
           break;
    }
    <component type="desktop">
      <name>Calendar</name>
      <description>
        <p>A slim, lightweight calendar app that syncs and manages multiple calendars in one place, like Google Calendar, Outlook and CalDAV.</p>
      </description>
      <releases>
        <release version="5.0" date="2019-02-28" urgency="medium">
          <description>
            <p>Add a search entry for calendars</p>
          </description>
        </release>
      </releases>
      <screenshots>
        <screenshot type="default">
          <image>https://raw.githubusercontent.com/elementary/calendar/master/data/screenshot.png</image>
        </screenshot>
      </screenshots>
    </component>
    /* User chose number five */
    if (c == 5) {
        a = 3;
        b = 4;
        d = -1;  // Values larger than 5 are undefined
    }
    /// TRANSLATORS: The first %s is search term, the second is the name of default browser
    title = _("Search for %s in %s").printf (query, get_default_browser_name ());
    var my_variable = 5;
    
    public void my_function_name () {
       do_stuff ();
    }
    public class UserListBox : Gtk.ListBox {
        private enum OperatingSystem {
            ELEMENTARY_OS,
            UBUNTU
        }
    }
    public const string UBUNTU = "ubuntu";
    
    private enum OperatingSystem {
        ELEMENTARY_OS,
        UBUNTU
    }
    public const string ACTION_GO_BACK = "action-go-back";
    /* OK */
    ((Gtk.Entry) widget).max_width_chars
    
    /* NOT OK as this approach requires a check for null */
    (widget as Gtk.Entry).max_width_chars
    set_can_focus (false);
    can_focus = false;
    var label = new Gtk.Label ("Test Label");
    label.set_ellipsize (Pango.EllipsizeMode.END);
    label.set_valign (Gtk.Align.END);
    label.set_width_chars (33);
    label.set_xalign (0);
    var label = new Gtk.Label ("Test Label") {
        ellipsize = Pango.EllipsizeMode.END,
        valign = Gtk.Align.END,
        width_chars = 33,
        xalign = 0
    };
    private int _number;
    
    public int get_number () {
        return _number;
    }
    
    public void set_number (int value) {
        _number = value;
    }
    public int number { get; set; }
    private int _number;
    public int number {
        get {
            // We can run extra code here before returning the property. For example,
            // we can multiply it by 2
            return _number * 2;
        }
        set {
            // We can run extra code here before/after updating the value. For example,
            // we could check the validity of the new value or trigger some other state
            // changes
            _number = value;
        }
    }
    GLib.print ("Hello World");
    print ("Hello World");
    var string = @"Error parsing config: $(config_path)";
    var string = "Error parsing config: %s".printf (config_path);
    critical ("Error parsing config: %s", config_path);
    var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (
        "Basic Information and a Suggestion",
        "Further details, including information that explains any unobvious consequences of actions.",
        "phone",
        Gtk.ButtonsType.CANCEL
    );
    # EditorConfig <https://EditorConfig.org>
    root = true
    
    # elementary defaults
    [*]
    charset = utf-8
    end_of_line = lf
    indent_size = tab
    indent_style = space
    insert_final_newline = true
    max_line_length = 80
    tab_width = 4
    
    # Markup files
    [{*.html,*.xml,*.xml.in,*.yml}]
    tab_width = 2
    Logo