Building Plugins:MythNotes05

From MythTV Official Wiki
Jump to: navigation, search

Stores full-text notes separately

  • SQL layout initialization
  • SQL layout update
  • Using SQL commands


Important.png Note: Read the MythNotes introduction Building_Plugins:MythNotes first.

Important.png Note: This tutorial follows-up the Building_Plugins:MythNotes02 tutorial.

C++ code

This example uses the complete plug-in structure including new SQL database creation / update scheme. You can see this structure in other plug-ins too.


Script.png /mythnotes/main.cpp

/*
 * Main of MythTV's demonstration plugin MythNotes
 *
 * Copyright (C) 2010 Lukas Doktor <ldoktor@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 */

#include <iostream>
using namespace std;

#include <QApplication>

#include "notesUI.h"
#include "dbcheck.h"


#include <mythversion.h>
#include <mythpluginapi.h>
#include <mythscreentype.h>
#include <mythuihelper.h>
#include <mythmainwindow.h>
#include <mythcontext.h>


#define LOC_ERR QString("MythNotes:MAIN Error: ")
#define LOC_WARN QString("MythNotes:MAIN Warning: ")
#define LOC QString("MythNotes:MAIN: ")

/* Main program run wrappers */
void runNotes(void);
int  RunNotes(void);

void setupKeys(void)
{
    REG_JUMP("MythNotes", QT_TRANSLATE_NOOP("MythControls",
        "Notes taking demonstration plugin"), "", runNotes);
    REG_KEY("MythNotes", "EXIT", QT_TRANSLATE_NOOP("MythControls",
    	"Exit the Smart Home"), "Q");
    REG_KEY("MythNotes", "MENU", QT_TRANSLATE_NOOP("MythControls",
    	"Delete note"), "M");
}

/* plugin initialization */
int mythplugin_init(const char *libversion)
{
	VERBOSE(VB_IMPORTANT, LOC + "init");
    if (!gContext->TestPopupVersion("mythnotes", libversion,
                                    MYTH_BINARY_VERSION))
    {
        VERBOSE(VB_IMPORTANT,
                QString("libmythnotes.so/main.o: binary version mismatch"));
        return -1;
    }

    gCoreContext->ActivateSettingsCache(false);
    if (!UpgradeDatabaseSchema())
	{
    	VERBOSE(VB_IMPORTANT,
    			"Couldn't upgrade database to new schema, exiting.");
    	return -1;
    }
    gCoreContext->ActivateSettingsCache(true);

    setupKeys();

    return 0;
}

void runNotes(void)
{
    RunNotes();
}

int RunNotes(void)
{
    MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack();
    NotesUI *notes = new NotesUI(mainStack, "Notes");
    
    if (notes->Create())
    {
        mainStack->AddScreen(notes);
        return 0;
    } else {
        delete notes;
        return -1;
    }
}

/* plugin execution */
int mythplugin_run(void)
{
	VERBOSE(VB_IMPORTANT, LOC + "exec");
    return RunNotes();
}

/* plugin config execution */
int mythplugin_config(void)
{
	VERBOSE(VB_IMPORTANT, LOC + "config");
    MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack();
    NotesUISettings *notes = new NotesUISettings(mainStack);

    if (notes->Create())
    {
        mainStack->AddScreen(notes);
        return 0;
    } else {
        delete notes;
        return -1;
    }
}

/* plugin clean-up */
void mythplugin_destroy(void)
{
	VERBOSE(VB_IMPORTANT, LOC + "destroy");
}
  • One important change is in the initialization during which the function "UpgradeDatabaseSchema()" is called. It check and creates or upgrade the database schema. It's placed in "dbcheck.cpp" which is described below.


Script.png /mythnotes/notesUI.h

/*
 * Header file for UI
 *
 * Copyright (C) 2010 Lukas Doktor <ldoktor@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 */

#ifndef NOTESUI_H_
#define NOTESUI_H_

#include <QString>
#include <QKeyEvent>

#include <mythscreentype.h>
#include <mythmainwindow.h>
#include <mythdialogbox.h>
#include <mythuibutton.h>
#include <mythuibuttonlist.h>
#include <mythdb.h>
#include <mythverbose.h>
#include <mythcontext.h>


class MythUIButton;
class MythUIButtonList;
class MythUIButtonListItem;
class MythUIText;
class MythUITextEdit;
class QKeyEvent;
class QEvent;

class NotesUI : public MythScreenType
{
	Q_OBJECT

  public:
	NotesUI(MythScreenStack *parentStack, QString name = "MythNotes");
	bool Create();
	bool keyPressEvent(QKeyEvent *event);
	void customEvent(QEvent *event);

	bool editDialog(int id=0);
	bool menuDialog();

  private:
	MythUIText			*m_title;
	MythUIText			*m_details;
	MythUIButtonList	*m_notes;

  private slots:
  	void notesCallback(MythUIButtonListItem *item);
  	void selectNotesCallback(MythUIButtonListItem *item);
	void closeCallback();
	void reloadNotes();
};

class NotesUISettings : public MythScreenType
{
	Q_OBJECT

  public:
	NotesUISettings(MythScreenStack *parentStack);
	bool Create();

  private:
	MythUIButton	*m_clearBtn;
  private slots:
	void clearCallback();
};

#endif /* NOTESUI_H_ */
  • Minor and already well known changes.


Script.png /mythnotes/notesUI.cpp

/*
 * Source file for UIs
 *
 * Copyright (C) 2010 Lukas Doktor <ldoktor@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 */

#include "notesUI.h"

#define LOC_ERR QString("MythNotes:notesUI Error: ")
#define LOC_WARN QString("MythNotes:notesUI Warning: ")
#define LOC QString("MythNotes:notesUI: ")

NotesUI::NotesUI(MythScreenStack *parent, QString name)
		: MythScreenType(parent, name), m_title(NULL), m_notes(NULL),
		  m_details(NULL)
{
}

bool NotesUI::Create()
{
	if (!LoadWindowFromXML("notes-ui.xml", "notesui", this))
		return false;

	bool err = false;
	UIUtilE::Assign(this, m_title, "title", &err);
	UIUtilE::Assign(this, m_notes, "notes", &err);
	UIUtilE::Assign(this, m_details, "details", &err);
	if (err)
		return false;


	connect(m_notes, SIGNAL(itemClicked(MythUIButtonListItem*)),
			this, SLOT(notesCallback(MythUIButtonListItem*)));
	connect(m_notes, SIGNAL(itemSelected(MythUIButtonListItem*)),
			this, SLOT(selectNotesCallback(MythUIButtonListItem*)));

	BuildFocusList();
	reloadNotes();
	SetFocusWidget(m_notes);

	m_details->SetText("");

	return true;
}

bool NotesUI::keyPressEvent(QKeyEvent *event)
{
    if (GetFocusWidget()->keyPressEvent(event))
        return true;

    bool handled = false;
    QStringList actions;
    handled = GetMythMainWindow()->TranslateKeyPress("MythNotes", event, actions);

    for (int i = 0; i < actions.size() && !handled; i++)
    {
        QString action = actions[i];
        handled = true;

        if (action == "ESCAPE") {
        	closeCallback();
        } else if (action == "MENU") {
        	menuDialog();
        } else {
            handled = false;
        }
    }

    if (!handled && MythScreenType::keyPressEvent(event))
        handled = true;

    return handled;
}

void NotesUI::customEvent(QEvent *event)
{
	if (event->type() == DialogCompletionEvent::kEventType)
	{
		DialogCompletionEvent *dce = (DialogCompletionEvent*)(event);

		if (dce->GetId() == "newNote")
		{
			MSqlQuery db(MSqlQuery::InitCon());
			QString query = QString("INSERT INTO mythnotes (note) "
									"VALUES ('%1');")
									.arg(dce->GetResultText());
			if (!db.exec(query)) {
				MythDB::DBError("New note", db);
			}
			reloadNotes();
		} else if (dce->GetId() == "editNote") {
			int id = m_notes->GetItemCurrent()->GetData().toInt();
			MSqlQuery db(MSqlQuery::InitCon());
			QString query = QString("UPDATE mythnotes "
									"SET note='%1' "
									"WHERE id='%2';")
									.arg(dce->GetResultText())
									.arg(id);
			if (!db.exec(query)) {
				MythDB::DBError("Edit note", db);
			}
			reloadNotes();
		} else if (dce->GetId() == "menu") {
			switch (dce->GetResult()) {
			case 0: { //Edit
				editDialog(m_notes->GetItemCurrent()->GetData().toInt());
				break;	}
			case 1: { //Reload
				reloadNotes();
			}
			case 2: { //Delete
				int id = m_notes->GetItemCurrent()->GetData().toInt();
				MSqlQuery db(MSqlQuery::InitCon());
				QString query = QString("DELETE FROM mythnotes "
										"WHERE id='%1';")
										.arg(id);
				if (!db.exec(query)) {
					MythDB::DBError("Delete note", db);
				}
				reloadNotes();
				break;	}
			case 3: { //Exit
				closeCallback();
				break;	}
			}
		}
	}
}

bool NotesUI::editDialog(int id)
{
	MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");

	if (!id) { //NEW
		MythTextInputDialog *dialog =
				new MythTextInputDialog(popupStack,
										"Insert your note");
		if (dialog->Create()) {
			popupStack->AddScreen(dialog);
			dialog->SetReturnEvent(this, "newNote");
		} else {
			delete dialog;
			return 0;
		}
	} else { //EDIT
		MSqlQuery db(MSqlQuery::InitCon());
		QString query = QString("SELECT note "
								"FROM mythnotes "
								"WHERE id='%1';")
								.arg(id);
		if (!db.exec(query)) {
			MythDB::DBError("Edit: Get by ID", db);
		}
		if (db.next()) {
			MythTextInputDialog *dialog =
					new MythTextInputDialog(popupStack,
											"Edit the note",
											(InputFilter)0,
											false,
											db.value(0).toString());
			if (dialog->Create()) {
				popupStack->AddScreen(dialog);
				dialog->SetReturnEvent(this, "editNote");
			} else {
				delete dialog;
				return 0;
			}
		}
	}
	return 1;
}

bool NotesUI::menuDialog()
{
	MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
	MythDialogBox *dialog = new MythDialogBox("Menu", popupStack, "menu");

	if (dialog->Create()) {
		popupStack->AddScreen(dialog);
		dialog->SetReturnEvent(this, "menu");
		dialog->AddButton("Edit Note");
		dialog->AddButton("Reload Notes");
		dialog->AddButton("Delete Note");
		dialog->AddButton("Exit MythNotes");
	} else {
		delete dialog;
		return 0;
	}
	return 1;
}

void NotesUI::notesCallback(MythUIButtonListItem *item)
{
	if (m_notes->GetCurrentPos() == 0) {
		editDialog();
	} else if (m_notes->GetCurrentPos() == 1) {
		closeCallback();
	} else {
		menuDialog();
	}
}

void NotesUI::selectNotesCallback(MythUIButtonListItem *item)
{
	if (m_notes->GetCurrentPos() < 2) {
		m_details->SetText("");
	} else {
		MSqlQuery db(MSqlQuery::InitCon());
		QString query = QString("SELECT note "
								"FROM mythnotes "
								"WHERE id='%1';")
								.arg(item->GetData().toInt());
		if (!db.exec(query)) {
			MythDB::DBError("select: Get by ID", db);
			m_details->SetText("ERROR, try reload notes");
		}
		if (db.next()) {
			m_details->SetText(db.value(0).toString());
		} else {
			m_details->SetText("ERROR, try reload notes");
		}
	}
}

void NotesUI::closeCallback()
{
	Close();
}

void NotesUI::reloadNotes()
{
	m_notes->Reset();
	new MythUIButtonListItem(m_notes, "Add note");
	new MythUIButtonListItem(m_notes, "Close MythNotes");

	MSqlQuery db(MSqlQuery::InitCon());
	QString query = QString("SELECT note,id "
							"FROM mythnotes;");
	if (!db.exec(query)) {
		MythDB::DBError("reload: Get by ID", db);
	}
	while (db.next()) {
		MythUIButtonListItem *item =
				new MythUIButtonListItem(m_notes, db.value(0).toString());
		item->SetData(db.value(1).toInt());
	}
}



NotesUISettings::NotesUISettings(MythScreenStack *parent)
				: MythScreenType(parent, "Notes UI Settings"), m_clearBtn(NULL)
{
}

bool NotesUISettings::Create()
{
	if (!LoadWindowFromXML("notes-ui.xml", "notesuisettings", this))
		return false;

	bool err = false;
	UIUtilE::Assign(this, m_clearBtn, "clearbtn", &err);
	if (err)
		return false;

	connect(m_clearBtn, SIGNAL(Clicked()), this, SLOT(clearCallback()));

	BuildFocusList();
	SetFocusWidget(m_clearBtn);

	return true;
}

void NotesUISettings::clearCallback()
{
}
  • You've already met all the C++/MythTV framework commands. Just look at the DB queries. In this example you can see the basic SQL commands like insert, update and delete. You can use additional commands like table locking too.

There are two new source files used to initialize the database before usage.


Script.png /mythnotes/dbcheck.h

/*
 * Header file for database check / initialization
 *
 * Copyright (C) 2010 Lukas Doktor <ldoktor@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 */

#ifndef DBCHECK_H_
#define DBCHECK_H_

#include <iostream>
using namespace std;

#include <QString>
#include <QSqlError>

#include <mythcontext.h>
#include <mythdb.h>

bool UpgradeDatabaseSchema(void);

#endif


Script.png /mythnotes/dbcheck.cpp

/*
 * Source file for database check / initialization
 *
 * Copyright (C) 2010 Lukas Doktor <ldoktor@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 */

#include "dbcheck.h"

const QString currentDatabaseVersion = "1000";

static bool UpdateDBVersionNumber(const QString &newnumber)
{

    if (!gContext->SaveSettingOnHost("NotesDBSchemaVer",newnumber,NULL))
    {
        VERBOSE(VB_IMPORTANT, QString("DB Error (Setting new DB version number): %1\n")
                              .arg(newnumber));

        return false;
    }

    return true;
}

static bool performActualUpdate(const QString updates[], QString version,
                                QString &dbver)
{
    MSqlQuery query(MSqlQuery::InitCon());

    VERBOSE(VB_IMPORTANT, QString("Upgrading to MythHome schema version ") +
            version);

    int counter = 0;
    QString thequery = updates[counter];

    while (thequery != "")
    {
        if (!query.exec(thequery))
        {
            QString msg =
                QString("DB Error (Performing database upgrade): \n"
                        "Query was: %1 \nError was: %2 \nnew version: %3")
                .arg(thequery)
                .arg(MythDB::DBErrorMessage(query.lastError()))
                .arg(version);
            VERBOSE(VB_IMPORTANT, msg);
            return false;
        }

        counter++;
        thequery = updates[counter];
    }

    if (!UpdateDBVersionNumber(version))
        return false;

    dbver = version;
    return true;
}

bool InitializeDatabase(void)
{
    VERBOSE(VB_IMPORTANT, "Inserting MythGame initial database information.");

    const QString updates[] = {
"CREATE TABLE mythnotes ("
"	id INT NOT NULL AUTO_INCREMENT,"
"	note VARCHAR(50),"
"   PRIMARY KEY (id)"
");",
""
};
    QString dbver = "";
    if (!performActualUpdate(updates, "1000", dbver))
    	return false;
    return true;
}

bool UpgradeDatabaseSchema(void)
{
    QString dbver = gContext->GetSetting("NotesDBSchemaVer");
    MSqlQuery query(MSqlQuery::InitCon());

    if (dbver == currentDatabaseVersion)
        return true;

    if (dbver.isEmpty())
    {
        if (!InitializeDatabase())
            return false;
        dbver = "1000";
    }
    return true;
}
  • The logic of this is simple. It defines the version and if the DB doesn't exists yet it creates new one. If it exists but doesn't match with the defined version it looks for the update scheme and incrementaly updates this layouts until the version matches. In this example only DB version "1000" is defined but you can see the examples of incremental update in other SVN plugins.
  • UpdateDBVersionNumber()
    • Stores the version number in gContext
  • performActualUpdate()
    • Takes the list of querries and executes them. At the end it stores the new DB version.
  • InitializeDatabase()
    • Creates new layout in version 1000. It actually defines the querry list and calls the performActualUpdate().
  • UpgradeDatabaseSchema()
    • The DB checking function used in the mythplugin_init() function.

UI files

Script.png /themes/default/notes-ui.xml

<?xml version="1.0" encoding="utf-8"?>
<mythuitheme>
    <window name="notesui">
        <textarea name="title">
            <area>10,10,780,60</area>
            <font>baselarge</font>
            <align>allcenter</align>
            <multiline>yes</multiline>
            <value>MythNotes</value>
        </textarea>

        <buttonlist name="notes" from="basebuttonlist">
        	<area>10,80,395,510</area>
        	<buttonarea>0,0,100%,100%</buttonarea>
        	<layout>vertical</layout>
        	<align>center</align>
        	<arrange>stack</arrange>
        	<statetype name="buttonitem">
        		<area>0,0,100%,30</area>
        	</statetype>
        	<showarrow>no</showarrow>
        </buttonlist>
        
        <textarea name="details">
        	<area>405,80,395,510</area>
        	<font>basemedium</font>
        	<align>top,left</align>
        	<multiline>yes</multiline>
        </textarea>
    </window>
    
    <window name="notesuisettings">
    	<textarea name="title">
            <area>10,10,780,60</area>
            <font>baselarge</font>
            <align>allcenter</align>
            <multiline>yes</multiline>
            <value>Notes Settings</value>
        </textarea>
    </window>
</mythuitheme>
  • No changes here

Remaining files

Script.png /mythnotes/mythnotes.pro

include ( ../../mythconfig.mak )
include ( ../../settings.pro )
include ( ../../programs-libs.pro )

TEMPLATE = lib
CONFIG += plugin \
    thread
TARGET = mythnotes
target.path = $${LIBDIR}/mythtv/plugins
INSTALLS += target
INCLUDEPATH += $${PREFIX}/include/mythtv/libmythui

# Input
HEADERS += notesUI.h \
	dbcheck.h
SOURCES += notesUI.cpp \
	dbcheck.cpp \
    main.cpp
QT += sql \
    xml \
    opengl
include ( ../../libs-targetfix.pro )
  • Don't forget to add the new HEADERS and SOURCES!

Compile & Run

See the first part Building_Plugins:MythNotes00.

Recapitulation

  • copy the plug-in from the first part (Building_Plugins:MythNotes04)
  • Modify the C++ code (new functions and files)
  • Add new headers and source files inclusion into "/mythnotes/mythnotes.pro"

Finish

Important.png Note: You have successfully created a complex plug-in. During the tutorial you've learend many MythTV framework commands and constructions which should help you in future MythTV plug-in develoment.

Thank you for reading. I hope you find this tutorials helpful.