diff --git a/mythtv/libs/libmythtv/osd.cpp b/mythtv/libs/libmythtv/osd.cpp
index 207ecef..59f721c 100644
--- a/mythtv/libs/libmythtv/osd.cpp
+++ b/mythtv/libs/libmythtv/osd.cpp
@@ -955,7 +955,8 @@ void OSD::DialogQuit(void)
     m_PulsedDialogText = QString();
 }
 
-void OSD::DialogShow(const QString &window, const QString &text, int updatefor)
+void OSD::DialogShow(const QString &window, const QString &text, int updatefor,
+                     const QString &hotMenuAction, bool handleAccel)
 {
     if (m_Dialog)
     {
@@ -982,6 +983,9 @@ void OSD::DialogShow(const QString &window, const QString &text, int updatefor)
             dialog = new ChannelEditor(m_ParentObject, window.toLatin1());
         else if (window == OSD_DLG_CONFIRM)
             dialog = new MythConfirmationDialog(NULL, text, false);
+        else if (!hotMenuAction.isEmpty())
+            dialog = new MythEditableDialogBox(text, NULL, window.toLatin1(),
+                                               hotMenuAction, handleAccel);
         else
             dialog = new MythDialogBox(text, NULL, window.toLatin1(), false, true);
 
diff --git a/mythtv/libs/libmythtv/osd.h b/mythtv/libs/libmythtv/osd.h
index a795469..5919617 100644
--- a/mythtv/libs/libmythtv/osd.h
+++ b/mythtv/libs/libmythtv/osd.h
@@ -12,6 +12,8 @@
 // menu dialogs should always be on top
 #define OSD_DLG_VIDEOEXIT "xx_OSD_VIDEO_EXIT"
 #define OSD_DLG_MENU      "xx_OSD_MENU"
+#define OSD_DLG_HOTMENU   "xx_OSD_HOTMENU"
+#define OSD_DLG_EDITHOTMENU "xx_OSD_EDITHOTMENU"
 #define OSD_DLG_SLEEP     "xx_OSD_SLEEP"
 #define OSD_DLG_IDLE      "xx_OSD_IDLE"
 #define OSD_DLG_INFO      "xx_OSD_INFO"
@@ -166,7 +168,9 @@ class OSD
     bool DialogHandleKeypress(QKeyEvent *e);
     void DialogQuit(void);
     void DialogShow(const QString &window, const QString &text = "",
-                    int updatefor = 0);
+                    int updatefor = 0,
+                    const QString &hotMenuAction = "",
+                    bool handleAccel = false);
     void DialogSetText(const QString &text);
     void DialogBack(QString text = "", QVariant data = 0, bool exit = false);
     void DialogAddButton(QString text, QVariant data,
diff --git a/mythtv/libs/libmythtv/tv_actions.h b/mythtv/libs/libmythtv/tv_actions.h
index e57c91f..8cdcb40 100644
--- a/mythtv/libs/libmythtv/tv_actions.h
+++ b/mythtv/libs/libmythtv/tv_actions.h
@@ -3,6 +3,8 @@
 
 #define ACTION_EXITSHOWNOPROMPTS "EXITSHOWNOPROMPTS"
 
+#define ACTION_HOTMENU "HOTMENU"
+
 #define ACTION_PLAYBACK      "PLAYBACK"
 #define ACTION_STOP          "STOPPLAYBACK"
 #define ACTION_DAYLEFT       "DAYLEFT"
diff --git a/mythtv/libs/libmythtv/tv_play.cpp b/mythtv/libs/libmythtv/tv_play.cpp
index 86f56af..37983fc 100644
--- a/mythtv/libs/libmythtv/tv_play.cpp
+++ b/mythtv/libs/libmythtv/tv_play.cpp
@@ -540,6 +540,8 @@ void TV::InitKeys(void)
     REG_KEY("TV Frontend", "CHANGEGROUPVIEW", QT_TRANSLATE_NOOP("MythControls",
             "Change Group View"), "");
 
+    REG_KEY("TV Playback", ACTION_HOTMENU, QT_TRANSLATE_NOOP("MythControls",
+            "Playback Custom Menu"), "");
     REG_KEY("TV Playback", "BACK", QT_TRANSLATE_NOOP("MythControls",
             "Exit or return to DVD menu"), "");
     REG_KEY("TV Playback", ACTION_CLEAROSD, QT_TRANSLATE_NOOP("MythControls",
@@ -1046,6 +1048,8 @@ void TV::InitFromDB(void)
     kv["VbiFormat"]                = "";
     kv["DecodeVBIFormat"]          = "";
 
+    kv["HotMenuActions"]           = "";
+
     int ff_rew_def[8] = { 3, 5, 10, 20, 30, 60, 120, 180 };
     for (uint i = 0; i < sizeof(ff_rew_def)/sizeof(ff_rew_def[0]); i++)
         kv[QString("FFRewSpeed%1").arg(i)] = QString::number(ff_rew_def[i]);
@@ -1087,6 +1091,8 @@ void TV::InitFromDB(void)
     db_remember_last_channel_group = kv["ChannelGroupRememberLast"].toInt();
     channelGroupId         = kv["ChannelGroupDefault"].toInt();
 
+    db_hotmenu_items       = kv["HotMenuActions"];
+
     QString beVBI          = kv["VbiFormat"];
     QString feVBI          = kv["DecodeVBIFormat"];
 
@@ -4306,6 +4312,8 @@ bool TV::ActiveHandleAction(PlayerContext *ctx,
 
         SetActive(ctx, 0, false);
     }
+    else if (has_action(ACTION_HOTMENU, actions))
+        ShowHotMenu(ctx, "");
     else if (has_action(ACTION_ENABLEUPMIX, actions))
         EnableUpmix(ctx, true);
     else if (has_action(ACTION_DISABLEUPMIX, actions))
@@ -8642,6 +8650,54 @@ void TV::customEvent(QEvent *e)
         return;
     }
 
+    if (e->type() == DialogEditEvent::kEventType)
+    {
+        DialogEditEvent *dee =
+            reinterpret_cast<DialogEditEvent*>(e);
+        QString item = dee->GetData().toString();
+        PlayerContext *mctx = GetPlayerReadLock(0, __FILE__, __LINE__);
+        OSD *osd = GetOSDLock(mctx);
+        if (osd && osd->DialogVisible(OSD_DLG_HOTMENU) && item != "MENU")
+        {
+            osd->DialogShow(OSD_DLG_EDITHOTMENU, tr("Edit Custom Menu Item") +
+                            "\n\n"  + ActionToString(item));
+            osd->DialogAddButton(tr("Move to Top"),
+                                 QString("HOTMENUEDIT_MOVETOP_%1")
+                                 .arg(item));
+            osd->DialogAddButton(tr("Move Up"),
+                                 QString("HOTMENUEDIT_MOVEUP_%1")
+                                 .arg(item));
+            osd->DialogAddButton(tr("Move Down"),
+                                 QString("HOTMENUEDIT_MOVEDOWN_%1")
+                                 .arg(item));
+            osd->DialogAddButton(tr("Move to Bottom"),
+                                 QString("HOTMENUEDIT_MOVEBOTTOM_%1")
+                                 .arg(item));
+            osd->DialogAddButton(tr("Remove"),
+                                 QString("HOTMENUEDIT_DELETE_%1")
+                                 .arg(item), false, (dee->GetResult() == -1));
+            osd->DialogBack(item, ACTION_HOTMENU, true);
+        }
+        else if (osd && osd->DialogVisible(OSD_DLG_MENU))
+        {
+            QString itemString = ActionToString(item, true);
+            if (!itemString.isEmpty() && !item.startsWith("DIALOG_MENU_"))
+            {
+                osd->DialogShow(OSD_DLG_EDITHOTMENU,
+                                ActionToString(item) + "\n\n" +
+                                tr("Add to Custom Menu?"));
+                osd->DialogAddButton(tr("OK"),
+                                     QString("HOTMENUADD_%1").arg(item),
+                                     false, true);
+                osd->DialogAddButton(tr("Cancel"), "HOTMENUCANCEL");
+                osd->DialogBack("", "HOTMENUCANCEL", true);
+            }
+        }
+        ReturnOSDLock(mctx, osd);
+        ReturnPlayerLock(mctx);
+        return;
+    }
+
     if (e->type() == OSDHideEvent::kEventType)
     {
         OSDHideEvent *ce = reinterpret_cast<OSDHideEvent*>(e);
@@ -10060,6 +10116,72 @@ void TV::OSDDialogEvent(int result, QString text, QString action)
             LOG(VB_GENERAL, LOG_ERR, "Unrecognised dialog event.");
         }
     }
+    else if (action == ACTION_HOTMENU)
+    {
+        hide = false;
+        ShowHotMenu(actx, text);
+    }
+    else if (action == "HOTMENUCANCEL")
+    {
+        // Bring back to the top-level menu, which isn't ideal.
+        hide = false;
+        ShowOSDMenu(actx);
+    }
+    else if (action.startsWith("HOTMENUADD_"))
+    {
+        action.remove("HOTMENUADD_");
+        QStringList itemList =
+            db_hotmenu_items.split(',', QString::SkipEmptyParts);
+        if (!itemList.contains(action))
+        {
+            if (!db_hotmenu_items.isEmpty())
+                db_hotmenu_items += ",";
+            db_hotmenu_items += action;
+            gCoreContext->SaveSetting("HotMenuActions", db_hotmenu_items);
+        }
+        hide = false;
+        ShowHotMenu(actx, action);
+    }
+    else if (action.startsWith("HOTMENUEDIT_"))
+    {
+        action = action.mid(12);
+        int separator = action.indexOf('_');
+        QString op = action.left(separator);
+        QString item = action.mid(separator + 1);
+        QStringList itemList =
+            db_hotmenu_items.split(',', QString::SkipEmptyParts);
+        int oldIndex = itemList.indexOf(item);
+        if (oldIndex >= 0)
+        {
+            int newIndex = oldIndex;
+            itemList.removeOne(item);
+            if (op == "MOVETOP")
+                newIndex = 0;
+            else if (op == "MOVEUP")
+                newIndex = (oldIndex > 0 ? oldIndex - 1 : 0);
+            else if (op == "MOVEDOWN")
+                newIndex = (oldIndex < itemList.size() ?
+                            oldIndex + 1 : itemList.size());
+            else if (op == "MOVEBOTTOM")
+                newIndex = itemList.size();
+            else if (op == "DELETE")
+                newIndex = -1;
+            if (newIndex >= 0)
+                itemList.insert(newIndex, item);
+        }
+        QString old = db_hotmenu_items;
+        db_hotmenu_items = "";
+        for (int i = 0; i < itemList.size(); ++i)
+        {
+            if (i > 0)
+                db_hotmenu_items += ",";
+            db_hotmenu_items += itemList[i];
+        }
+        if (db_hotmenu_items != old)
+            gCoreContext->SaveSetting("HotMenuActions", db_hotmenu_items);
+        hide = false;
+        ShowHotMenu(actx, item);
+    }
     else if (result < 0)
         ; // exit dialog
     else if (HandleTrackAction(actx, action))
@@ -10319,6 +10441,11 @@ void TV::OSDDialogEvent(int result, QString text, QString action)
             DoQueueTranscode(actx, "Medium Quality");
         else if (action == "QUEUETRANSCODE_LOW")
             DoQueueTranscode(actx, "Low Quality");
+        else if (action == "MENU")
+        {
+            hide = false;
+            ShowOSDMenu(actx);
+        }
         else
         {
             LOG(VB_GENERAL, LOG_ERR, LOC +
@@ -10359,6 +10486,121 @@ void TV::HandleOSDInfo(PlayerContext *ctx, QString action)
     }
 }
 
+QString TV::ActionToString(const QString &action, bool strict)
+{
+    static QHash<QString, QString> map;
+    if (map.isEmpty())
+    {
+        // AUDIO
+        map[ACTION_TOGGELAUDIOSYNC] = tr("Adjust Audio Sync");
+        map[ACTION_DISABLEUPMIX] = tr("Disable Audio Upmixer");
+        map[ACTION_ENABLEUPMIX] = tr("Enable Audio Upmixer");
+        // don't do AUDIO:AUDIOTRACKS
+        // don't do AUDIO:VISUALISATIONS
+        // VIDEO
+        map["TOGGLEMANUALZOOM"] = tr("Manual Zoom Mode");
+        // VIDEO:VIDEOASPECT
+        for (int i = kAspect_Off; i < kAspect_END; i++)
+        {
+            map[QString("TOGGLEASPECT%1").arg(i)] =
+                tr("Aspect Ratio") + " - " +
+                toString((AspectOverrideMode) i);
+        }
+        // VIDEO:ADJUSTFILL
+        map["TOGGLEFILL"] = tr("Next Zoom Mode");
+        for (int i = kAdjustFill_Off; i < kAdjustFill_END; i++)
+        {
+            map[QString("TOGGLEFILL%1").arg(i)] =
+                tr("Adjust Fill") + " - " + toString((AdjustFillMode) i);
+        }
+        // VIDEO:ADJUSTPICTURE
+        map[ACTION_TOGGLENIGHTMODE] = tr("Toggle night mode");
+        // VIDEO:3D
+        map[ACTION_3DNONE]                = tr("No 3D");
+        map[ACTION_3DSIDEBYSIDE]          = tr("3D Side by Side");
+        map[ACTION_3DSIDEBYSIDEDISCARD]   = tr("Discard 3D Side by Side");
+        map[ACTION_3DTOPANDBOTTOM]        = tr("3D Top and Bottom");
+        map[ACTION_3DTOPANDBOTTOMDISCARD] = tr("Discard 3D Top and Bottom");
+        // VIDEO:ADVANCEDVIDEO
+        // VIDEO:ADVANCEDVIDEO:DEINTERLACER
+        // VIDEO:ADVANCEDVIDEO:VIDEOSCAN
+        QString vs = tr("Video Scan");
+        map["SELECTSCAN_0"] = vs + " - " + tr("Detect");
+        map["SELECTSCAN_1"] = vs + " - " + tr("Interlaced (Normal)");
+        map["SELECTSCAN_2"] = vs + " - " + tr("Interlaced (Reversed)");
+        map["SELECTSCAN_3"] = vs + " - " + tr("Progressive");
+        // SUBTITLES
+        map[ACTION_DISABLESUBS] = tr("Disable Subtitles");
+        map[ACTION_ENABLESUBS]  = tr("Enable Subtitles");
+        // NAVIGATE:COMMSKIP
+        for (int i = 0; i < kCommSkipCount ; ++i)
+            map[QString("TOGGLECOMMSKIP%1").arg(i)] =
+                toString((CommSkipMode)i);
+        // JOBS
+        map["EDIT"] = tr("Edit Recording");
+        map["TOGGLEAUTOEXPIRE"] = tr("Toggle Auto-Expire");
+        // PLAYBACK
+        map[ACTION_PAUSE] = tr("Pause");
+        map[ACTION_TOGGLEOSDDEBUG] = tr("Playback data");
+        // PLAYBACK:TIMESTRETCH
+        const QString adjts = tr("Time Stretch");
+        map["ADJUSTSTRETCH"]    = tr("Adjust Time Stretch");
+        map["TOGGLESTRETCH"]    = adjts + " - " + tr("Toggle");
+        map["ADJUSTSTRETCH0.5"] = adjts + " - " + tr("0.5X");
+        map["ADJUSTSTRETCH0.9"] = adjts + " - " + tr("0.9X");
+        map["ADJUSTSTRETCH1.0"] = adjts + " - " + tr("1.0X");
+        map["ADJUSTSTRETCH1.1"] = adjts + " - " + tr("1.1X");
+        map["ADJUSTSTRETCH1.2"] = adjts + " - " + tr("1.2X");
+        map["ADJUSTSTRETCH1.3"] = adjts + " - " + tr("1.3X");
+        map["ADJUSTSTRETCH1.4"] = adjts + " - " + tr("1.4X");
+        map["ADJUSTSTRETCH1.5"] = adjts + " - " + tr("1.5X");
+        // SCHEDULE
+        map[ACTION_GUIDE]         = tr("Program Guide");
+        map[ACTION_FINDER]        = tr("Program Finder");
+        map[ACTION_VIEWSCHEDULED] = tr("Upcoming Recordings");
+        map["SCHEDULE"]           = tr("Edit Recording Schedule");
+
+        //map["TOGGLEFILL"] = toString(kAdjustFill_Toggle);
+    }
+    if (map.contains(action))
+        return map[action];
+    else if (strict)
+        return "";
+    else
+        return action.left(1) + action.mid(1).toLower();
+}
+
+void TV::ShowHotMenu(const PlayerContext *ctx, const QString &itemSelected)
+{
+    if (ctx->playingInfo->QueryIsEditing())
+        return;
+    OSD *osd = GetOSDLock(ctx);
+    if (osd)
+    {
+        QStringList itemList =
+            db_hotmenu_items.split(',', QString::SkipEmptyParts);
+        itemList += "MENU";
+        osd->DialogShow(OSD_DLG_HOTMENU, tr("Playback Custom Menu"), 0,
+                        ACTION_HOTMENU, true);
+        int num;
+        for (num = 0; num < itemList.size(); ++num)
+        {
+            int displayNum = num + 1;
+            if (displayNum == 10)
+                displayNum = 0;
+            else if (displayNum > 10)
+                displayNum = -1;
+            QString prefix = "";
+            if (displayNum >= 0)
+                prefix = QString("[%1] ").arg(displayNum);
+            QString action = itemList[num];
+            osd->DialogAddButton(prefix + ActionToString(itemList[num]),
+                                 action, false, action == itemSelected);
+        }
+    }
+    ReturnOSDLock(ctx, osd);
+}
+
 void TV::ShowOSDMenu(const PlayerContext *ctx, const QString category,
                      const QString selected)
 {
@@ -10367,7 +10609,7 @@ void TV::ShowOSDMenu(const PlayerContext *ctx, const QString category,
     OSD *osd = GetOSDLock(ctx);
     if (osd)
     {
-        osd->DialogShow(OSD_DLG_MENU, tr("Playback Menu"));
+        osd->DialogShow(OSD_DLG_MENU, tr("Playback Menu"), 0, ACTION_HOTMENU);
         QString currenttext = QString();
         QString back = QString();
 
diff --git a/mythtv/libs/libmythtv/tv_play.h b/mythtv/libs/libmythtv/tv_play.h
index d7b22ab..cf8ab60 100644
--- a/mythtv/libs/libmythtv/tv_play.h
+++ b/mythtv/libs/libmythtv/tv_play.h
@@ -171,6 +171,7 @@ class MTV_PUBLIC TV : public QObject
     static void ReloadKeys(void);
     static void SetFuncPtr(const char *, void *);
     static int  ConfiguredTunerCards(void);
+    static QString ActionToString(const QString &action, bool strict = false);
 
     /// \brief Helper class for Sleep Timer code.
     class SleepTimerInfo
@@ -570,6 +571,7 @@ class MTV_PUBLIC TV : public QObject
     // Menu dialog
     void ShowOSDMenu(const PlayerContext*, const QString category = "",
                      const QString selected = "");
+    void ShowHotMenu(const PlayerContext *ctx, const QString &itemSelected);
 
     void FillOSDMenuAudio    (const PlayerContext *ctx, OSD *osd,
                               QString category, const QString selected,
@@ -636,6 +638,7 @@ class MTV_PUBLIC TV : public QObject
     bool    db_use_channel_groups;
     bool    db_remember_last_channel_group;
     ChannelGroupList db_channel_groups;
+    QString db_hotmenu_items;
 
     CommSkipMode autoCommercialSkip;
     bool    tryUnflaggedSkip;
diff --git a/mythtv/libs/libmythui/mythdialogbox.cpp b/mythtv/libs/libmythui/mythdialogbox.cpp
index 625aa1e..c35d3a0 100644
--- a/mythtv/libs/libmythui/mythdialogbox.cpp
+++ b/mythtv/libs/libmythui/mythdialogbox.cpp
@@ -24,6 +24,8 @@
 
 QEvent::Type DialogCompletionEvent::kEventType =
     (QEvent::Type) QEvent::registerEventType();
+QEvent::Type DialogEditEvent::kEventType =
+    (QEvent::Type) QEvent::registerEventType();
 
 
 MythMenu::MythMenu(const QString &text, QObject *retobject, const QString &resultid) :
@@ -444,6 +446,63 @@ void MythDialogBox::SendEvent(int res, QString text, QVariant data)
 
 /////////////////////////////////////////////////////////////////
 
+bool MythEditableDialogBox::keyPressEvent(QKeyEvent *event)
+{
+    if (GetFocusWidget()->keyPressEvent(event))
+        return true;
+
+    bool handled = false;
+    QStringList actions;
+    handled = GetMythMainWindow()->
+        TranslateKeyPress("TV Playback", event, actions);
+    for (int i = 0; i < actions.size() && !handled; i++)
+    {
+        QString action = actions[i];
+        MythUIButtonListItem *item = m_buttonList->GetItemCurrent();
+        QString text = item->GetText();
+        QVariant data = item->GetData();
+        if (action == m_editAction)
+        {
+            handled = true;
+            int res = m_buttonList->GetCurrentPos();
+            DialogEditEvent *dee = new DialogEditEvent(m_id, res, text, data);
+            QCoreApplication::postEvent(m_retObject, dee);
+        }
+        else if (m_handleAccel)
+        {
+            if (action.length() == 1 &&
+                action >= ACTION_0 && action <= ACTION_9)
+            {
+                int digit = action.at(0).digitValue(); // -1 if not a digit
+                // Treat '1' as the first item, '0' as the 10th item
+                if (digit == 0)
+                    digit = 10;
+                MythUIButtonListItem *item =
+                    m_buttonList->GetItemAt(digit - 1);
+                if (item)
+                {
+                    handled = true;
+                    Select(item);
+                }
+            }
+            else if (action == "DELETE")
+            {
+                handled = true;
+                DialogEditEvent *dee =
+                    new DialogEditEvent(m_id, -1, text, data);
+                QCoreApplication::postEvent(m_retObject, dee);
+            }
+        }
+    }
+
+    if (!handled && MythDialogBox::keyPressEvent(event))
+        return true;
+
+    return handled;
+}
+
+/////////////////////////////////////////////////////////////////
+
 MythConfirmationDialog::MythConfirmationDialog(MythScreenStack *parent,
                                                const QString &message,
                                                bool showCancel)
diff --git a/mythtv/libs/libmythui/mythdialogbox.h b/mythtv/libs/libmythui/mythdialogbox.h
index 06c3271..d84c28c 100644
--- a/mythtv/libs/libmythui/mythdialogbox.h
+++ b/mythtv/libs/libmythui/mythdialogbox.h
@@ -51,6 +51,28 @@ class MUI_PUBLIC DialogCompletionEvent : public QEvent
     QVariant m_resultData;
 };
 
+class MUI_PUBLIC DialogEditEvent : public QEvent
+{
+  public:
+    DialogEditEvent(const QString &id, int result, QString text,
+                    QVariant data)
+        : QEvent(kEventType),
+        m_id(id), m_result(result), m_resultText(text), m_resultData(data) { }
+
+    QString GetId() { return m_id; }
+    int GetResult() { return m_result; }
+    QString GetResultText() { return m_resultText; }
+    QVariant GetData() { return m_resultData; }
+
+    static Type kEventType;
+
+  private:
+    QString m_id;
+    int m_result;
+    QString m_resultText;
+    QVariant m_resultData;
+};
+
 
 class MUI_PUBLIC MythMenuItem
 {
@@ -170,6 +192,21 @@ class MUI_PUBLIC MythDialogBox : public MythScreenType
     MythMenu *m_currentMenu;
 };
 
+class MUI_PUBLIC MythEditableDialogBox : public MythDialogBox
+{
+    Q_OBJECT
+  public:
+    MythEditableDialogBox(const QString &text,
+                     MythScreenStack *parent, const char *name,
+                     const QString &editAction, bool handleAccel) :
+        MythDialogBox(text, parent, name, false/*fullscreen*/, true/*osd*/),
+        m_editAction(editAction), m_handleAccel(handleAccel) { }
+    virtual bool keyPressEvent(QKeyEvent *event);
+ protected:
+    const QString m_editAction; // action that initiates a DialogEditEvent
+    bool m_handleAccel; // whether to handle accelerators: DELETE, 0, ..., 9
+};
+
 
 /**
  *  \class MythConfirmationDialog
