Easyhacking: How to create a new “Tip-Of-The-Day” dialog

LibreOffice is an application with a large number of expert features, and though aimed to be easy to use there are always surprising shortcuts to achieve a goal. We post every day a tip on Twitter, and with the upcoming release 6.3 there will be also a tip-of-the-day messagebox when you start the program. This post aims to show how such a simple messagebox can be implemented (the complete patch is here).

The Dialog

Unless you are an experienced developer it’s a good idea to take a look on how things are implemented. As basis for this project we can use the About dialog.

Amateurs start with visuals first, so let’s take a look at the ui file – and that’s very easy since you can use Glade to create the dialog

The requirements are:

  • The dialog should have a title and static text for the tip
  • Additional information should be accessible via online/offline help or any other website
  • It should be possible to illustrate the actual tip with a pleasant image
  • Expert users want to easily disable (and reactivate) the dialog

In Glade, we add a GtkBox to arrange the objects, insert a GtkButtonBox for the footer and add a GtkCheckbox and two GtkButton to it. Then another GtkBox for the GtkImage left-hand and GtkLabels and GtkLinkButton to the right. Some of the object properties need to be adjusted – but that’s very straightforward.

Dialog in Glade

Figure 1: The dialog in Glade

Save the file at cui/uiconfig/ui/ where the aboutdialog is also stored.

Down to the nitty-gritty

Now we need a few lines of code to show it. This code goes into a new c++ file that is stored at cui/source/dialogs/ and the header file at cui/source/inc (all just like it’s done for the About dialog). In the header file, we create a new class and add variables for the controls and functions:

#ifndef INCLUDED_CUI_SOURCE_INC_TIPOFTHEDAYDLG_HXX
#define INCLUDED_CUI_SOURCE_INC_TIPOFTHEDAYDLG_HXX

#include <vcl/weld.hxx>

class TipOfTheDayDialog : public weld::GenericDialogController
{
private:
    std::unique_ptr<weld::Image> m_pImage;
    std::unique_ptr<weld::Label> m_pText;
...
public:
    TipOfTheDayDialog(weld::Window* pWindow);
    virtual ~TipOfTheDayDialog() override;
};

Learn more about welding at Caolán McNamara’s blog posts.
We include this header in the C++ file and load the ui file at the constructor:

#include <tipofthedaydlg.hxx>
...

TipOfTheDayDialog::TipOfTheDayDialog(weld::Window* pParent)
    : GenericDialogController(pParent, "cui/ui/tipofthedaydialog.ui", "TipOfTheDayDialog")
    , m_pImage(m_xBuilder->weld_image("imImage"))
    , m_pText(m_xBuilder->weld_label("lbText"))
    , m_pShowTip(m_xBuilder->weld_check_button("cbShowTip"))
    , m_pNext(m_xBuilder->weld_button("btnNext"))
    , m_pLink(m_xBuilder->weld_link_button("btnLink"))
{
...

It’s more or less explanatory: the variable names are linked to the IDs from the dialog using the correct weld function.
To build the new code, we finally need to add the new files to the makefiles (cui/Library_cui.mk and cui/UIConfig_cui.mk).

Loading the strings

The big advantage of an inbuilt tip-of-the-day dialog is the localization of the tips (apologies to the l10n team who has to translate all the text). We have to create another include file with the extension hrc (see also All About Terminology). And since it should be possible to assign a link and an image to every string, we combine all into a tuple and define the tip as a string array.

#ifndef INCLUDED_CUI_INC_TIPOFTHEDAY_HRC
#define INCLUDED_CUI_INC_TIPOFTHEDAY_HRC
#define NC_(Context, String) reinterpret_cast<char const *>(Context "\004" u8##String)
#include <tuple>

const std::tuple<const char*, OUString, OUString> TIPOFTHEDAY_STRINGARRAY[] =
{
     { NC_("RID_CUI_TIPOFTHEDAY", "%PRODUCTNAME can open and save files stored on remote servers per CMIS."), "fps/ui/remotefilesdialog/RemoteFilesDialog", ""}, //https://help.libreoffice.org/6.2/en-US/text/shared/guide/cmis-remote-files.html
...

The weird link fps/ui… points to the offline help (being forwarded to the online help site if not installed). Read the comments on top of the hrc file to learn how to find the right link.
The strings are loaded in the C++ file:

void TipOfTheDayDialog::UpdateTip()
{
    // text
    OUString aText = CuiResId(std::get<0>(TIPOFTHEDAY_STRINGARRAY[nCurrentTip]));
    m_pText->set_label(aText);

    // hyperlink
    aLink = std::get<1>(TIPOFTHEDAY_STRINGARRAY[nCurrentTip]);
    m_pLink->set_uri(aLink);
    ...
    // image
    OUString aURL("$BRAND_BASE_DIR/$BRAND_SHARE_SUBDIR/tipoftheday/");
    OUString aImage = std::get<2>(TIPOFTHEDAY_STRINGARRAY[nCurrentTip]);
    ...
    if (GraphicFilter::LoadGraphic(aURL + aImage, OUString(), aGraphic) == ERRCODE_NONE)
    { ...
}

The function contains a few more lines to hide the link when nothing is specified and to load and draw the image. And another function and more code is required to make the dialog optional. See also the posting on How To Make A Feature Optional.
UpdateTip() is executed when the dialog is instantiated (and on click at Next Tip) with a random number to avoid bloating the registry with the last used tip.

Showing the dialog on start-up

Admittedly, to find a good place for showing the dialog on start-up is not so easy. It is done in SfxViewFrame::Notify() at sfx2/source/view/viewfrm.cxx. Taking the example of the About dialog, we have to define the class as an abstract prototype in cui/source/factory/dlgfact.hxx. With the abstract class we can now call the dialog with the restriction to show up only once per day and of course only when the user hasn’t disabled it:

const bool bShowTipOfTheDay = officecfg::Office::Common::Misc::ShowTipOfTheDay::get();
if (bShowTipOfTheDay && !Application::IsHeadlessModeEnabled() && !bIsUITest) {
    const sal_Int32 nLastTipOfTheDay = officecfg::Office::Common::Misc::LastTipOfTheDayShown::get();
    const sal_Int32 nDay = std::chrono::duration_cast<std::chrono::hours>(t0).count()/24; // days since 1970-01-01
    if (nDay-nLastTipOfTheDay > 0) { //only once per day
        VclAbstractDialogFactory* pFact = VclAbstractDialogFactory::Create();
        ScopedVclPtr<VclAbstractDialog> pDlg(
            pFact->CreateTipOfTheDayDialog(GetWindow().GetFrameWeld()));
        pDlg->Execute();
    }
}

Looks difficult? It is! You are welcome to ask for help at the IRC channel (thanks a lot to caolan, mikekaganski, and sberg). See also the wiki article on Create new dialog in Impress.

Contribute with more simple changes

As a community project we appreciate any kind of help. So if all this sounds too complicated, why don’t you look through the tips and make screenshots or draw illustrations? It’s quite challenging to show the essence of a tip on 100x120px. When done, change the respective tip and add new images to the makefile (read the comments in the hrc file for a detailled how-to). Or maybe you have ideas for more tips: Just add the line at the end of array. Don’t hesitate to ask the design team or the developers for support.