From 626df4db058d09aab5cbce849bdd6e580c83b0fc Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Sat, 27 Aug 2011 06:29:36 +0000 Subject: [PATCH] Rewrite the dynamic menu generation code Remove hardcoded assumptions about where in the menu items are and instead bind menu items directly to commands so that customizing the menu actually works. Add support for user menu files that override the default one. Add better support for multiple menus so that all of the menus can potentially be created by the dynamic menu system rather than just the main menu bar. Add support for commands whose names change based on the current project state so that undo and redo can work properly. Simplify the menu json format and make commands responsible for controlling what type of menu item is created rather than allowing nonsensical configurations. The Automation menu is currently not implemented. Originally committed to SVN as r5554. --- .../aegisub_vs2008/aegisub_vs2008.vcproj | 4 - aegisub/src/command/command.cpp | 3 +- aegisub/src/command/command.h | 17 +- aegisub/src/command/edit.cpp | 18 +- aegisub/src/command/menu_.cpp | 90 --- aegisub/src/command/recent.cpp | 20 +- aegisub/src/frame_main.cpp | 236 +----- aegisub/src/frame_main.h | 5 - aegisub/src/include/aegisub/menu.h | 73 +- aegisub/src/libresrc/default_menu.json | 751 ++++-------------- aegisub/src/main.cpp | 6 +- aegisub/src/menu.cpp | 375 ++++++--- aegisub/src/subs_grid.cpp | 2 +- aegisub/src/toolbar.cpp | 2 +- aegisub/src/utils.h | 1 - 15 files changed, 544 insertions(+), 1059 deletions(-) delete mode 100644 aegisub/src/command/menu_.cpp diff --git a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj index 376c31dfa..fc9754759 100644 --- a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj +++ b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj @@ -1915,10 +1915,6 @@ RelativePath="..\..\src\command\keyframe.cpp" > - - diff --git a/aegisub/src/command/command.cpp b/aegisub/src/command/command.cpp index 875d265a5..721fdc457 100644 --- a/aegisub/src/command/command.cpp +++ b/aegisub/src/command/command.cpp @@ -19,6 +19,7 @@ /// @ingroup command #include "command.h" + #include "icon.h" #include @@ -85,7 +86,6 @@ namespace cmd { void init_grid(); void init_help(); void init_keyframe(); - void init_menu(); void init_recent(); void init_subtitle(); void init_time(); @@ -102,7 +102,6 @@ namespace cmd { init_grid(); init_help(); init_keyframe(); - init_menu(); init_recent(); init_subtitle(); init_time(); diff --git a/aegisub/src/command/command.h b/aegisub/src/command/command.h index 4b3e1b8d3..52eba5a03 100644 --- a/aegisub/src/command/command.h +++ b/aegisub/src/command/command.h @@ -37,8 +37,8 @@ DEFINE_SIMPLE_EXCEPTION_NOINNER(CommandIconNone, CommandError, "command/icon") DEFINE_SIMPLE_EXCEPTION_NOINNER(CommandIconInvalid, CommandError, "command/icon/invalid") #define CMD_NAME(a) const char* name() { return a; } -#define STR_MENU(a) wxString StrMenu() const { return a; } -#define STR_DISP(a) wxString StrDisplay() const { return a; } +#define STR_MENU(a) wxString StrMenu(const agi::Context *) const { return a; } +#define STR_DISP(a) wxString StrDisplay(const agi::Context *) const { return a; } #define STR_HELP(a) wxString StrHelp() const { return a; } #define CMD_TYPE(a) int Type() const { using namespace cmd; return a; } @@ -85,10 +85,15 @@ namespace cmd { /// Holds an individual Command class Command { public: - virtual const char* name()=0; ///< Command name. - virtual wxString StrMenu() const=0; ///< String for menu purposes including accelerators. - virtual wxString StrDisplay() const=0; ///< Plain string for display purposes. - virtual wxString StrHelp() const=0; ///< Short help string descripting the command purpose. + /// Command name + virtual const char* name()=0; + /// String for menu purposes including accelerators, but not hotkeys + virtual wxString StrMenu(const agi::Context *) const=0; + /// Plain string for display purposes; should normally be the same as StrMenu + /// but without accelerators + virtual wxString StrDisplay(const agi::Context *) const=0; + /// Short help string descripting the command purpose. + virtual wxString StrHelp() const=0; /// Get this command's type flags /// @return Bitmask of CommandFlags diff --git a/aegisub/src/command/edit.cpp b/aegisub/src/command/edit.cpp index 3db1252a9..ad2053211 100644 --- a/aegisub/src/command/edit.cpp +++ b/aegisub/src/command/edit.cpp @@ -347,11 +347,16 @@ struct edit_line_swap : public Command { /// Redoes last action. struct edit_redo : public Command { CMD_NAME("edit/redo") - STR_MENU("&Redo") - STR_DISP("Redo") STR_HELP("Redoes last action.") CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME) + wxString StrMenu(const agi::Context *c) const { + return wxString::Format(_("&Redo %s"), c->ass->GetRedoDescription()); + } + wxString StrDisplay(const agi::Context *c) const { + return wxString::Format(_("Redo %s"), c->ass->GetRedoDescription()); + } + bool Validate(const agi::Context *c) { return !c->ass->IsRedoStackEmpty(); } @@ -380,11 +385,16 @@ struct edit_search_replace : public Command { /// Undoes last action. struct edit_undo : public Command { CMD_NAME("edit/undo") - STR_MENU("&Undo") - STR_DISP("Undo") STR_HELP("Undoes last action.") CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME) + wxString StrMenu(const agi::Context *c) const { + return wxString::Format(_("&Undo %s"), c->ass->GetUndoDescription()); + } + wxString StrDisplay(const agi::Context *c) const { + return wxString::Format(_("Undo %s"), c->ass->GetUndoDescription()); + } + bool Validate(const agi::Context *c) { return !c->ass->IsUndoStackEmpty(); } diff --git a/aegisub/src/command/menu_.cpp b/aegisub/src/command/menu_.cpp deleted file mode 100644 index eb4410b81..000000000 --- a/aegisub/src/command/menu_.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2010, Amar Takhar -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the Aegisub Group nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// Aegisub Project http://www.aegisub.org/ -// -// $Id$ - -/// @file menu.cpp -/// @brief menu/ commands, related to activating/deactivating/populating menu items -/// @ingroup command -/// - -#include "../config.h" - -#ifndef AGI_PRE -#endif - -#include "command.h" - -#include "../include/aegisub/context.h" - -namespace { - using cmd::Command; -/// @defgroup cmd-menu Main menu dropdown and submenu related commands. -/// @{ - -COMMAND_GROUP(main_audio, "main/audio", "Audio", "Audio", "Audio manipulation."); -COMMAND_GROUP(main_automation, "main/automation", "Automation", "Automation", "Automation manipulation and scripts."); -COMMAND_GROUP(main_edit, "main/edit", "&Edit", "Edit", "Editing operations."); -COMMAND_GROUP(main_edit_sort_lines, "main/edit/sort lines", "Sort lines", "Sort lines", "Sort lines by column."); -COMMAND_GROUP(main_file, "main/file", "&File", "File", "Operations on subtitles."); -COMMAND_GROUP(main_help, "main/help", "Help", "Help", "Help options."); -COMMAND_GROUP(main_subtitle, "main/subtitle", "&Subtitle", "Subtitle", "Subtitle manipulation."); -COMMAND_GROUP(main_subtitle_insert_lines, "main/subtitle/insert lines", "&Insert Lines", "Insert Lines", "Insert lines into currently active subtitle file."); -COMMAND_GROUP(main_subtitle_sort_lines, "main/subtitle/sort lines", "Sort Lines", "Sort Lines", "Sort lines by column."); -COMMAND_GROUP(main_subtitle_join_lines, "main/subtitle/join lines", "Join Lines", "Join Lines", "Merge 2 or more lines together."); -COMMAND_GROUP(main_timing, "main/timing", "&Timing", "Timing", "Time manipulation."); -COMMAND_GROUP(main_timing_make_times_continuous, "main/timing/make times continuous", "Make Times Continuous", "Make Times Continuous", "Make time continuous."); -COMMAND_GROUP(main_video, "main/video", "&Video", "Video", "Video operations."); -COMMAND_GROUP(main_video_override_ar, "main/video/override ar", "Override AR", "Override AR", "Override Aspect Ratio"); -COMMAND_GROUP(main_video_set_zoom, "main/video/set zoom", "Set Zoom", "Set Zoom", "Set zoom level."); -COMMAND_GROUP(main_view, "main/view", "View", "View", "View options."); -} - -/// @} - -namespace cmd { - void init_menu() { - reg(new main_audio); - reg(new main_automation); - reg(new main_edit); - reg(new main_edit_sort_lines); - reg(new main_file); - reg(new main_help); - reg(new main_subtitle); - reg(new main_subtitle_insert_lines); - reg(new main_subtitle_join_lines); - reg(new main_subtitle_sort_lines); - reg(new main_timing); - reg(new main_timing_make_times_continuous); - reg(new main_video); - reg(new main_video_override_ar); - reg(new main_video_set_zoom); - reg(new main_view); - } -} diff --git a/aegisub/src/command/recent.cpp b/aegisub/src/command/recent.cpp index e0888bc4f..db26d631e 100644 --- a/aegisub/src/command/recent.cpp +++ b/aegisub/src/command/recent.cpp @@ -57,9 +57,9 @@ namespace { /// @{ COMMAND_GROUP(recent_audio, "recent/audio", "Recent", "Recent", "Open recent audio."); -COMMAND_GROUP(recent_keyframe, "recent/keyframe", "Recent", "Recent", "Open recent keyframes."); +COMMAND_GROUP(recent_keyframes, "recent/keyframe", "Recent", "Recent", "Open recent keyframes."); COMMAND_GROUP(recent_subtitle, "recent/subtitle", "Recent", "Recent", "Open recent subtitles."); -COMMAND_GROUP(recent_timecode, "recent/timecode", "Recent", "Recent", "Open recent timecodes."); +COMMAND_GROUP(recent_timecodes, "recent/timecodes", "Recent", "Recent", "Open recent timecodes."); COMMAND_GROUP(recent_video, "recent/video", "Recent", "Recent", "Open recent video."); struct recent_audio_entry : public Command { @@ -73,8 +73,8 @@ struct recent_audio_entry : public Command { } }; -struct recent_keyframe_entry : public Command { - CMD_NAME("recent/keyframe/") +struct recent_keyframes_entry : public Command { + CMD_NAME("recent/keyframes/") STR_MENU("Recent") STR_DISP("Recent") STR_HELP("Open recent keyframes.") @@ -95,8 +95,8 @@ struct recent_subtitle_entry : public Command { } }; -struct recent_timecode_entry : public Command { - CMD_NAME("recent/timecode/") +struct recent_timecodes_entry : public Command { + CMD_NAME("recent/timecodes/") STR_MENU("Recent") STR_DISP("Recent") STR_HELP("Open recent timecodes.") @@ -141,17 +141,17 @@ public: namespace cmd { void init_recent() { reg(new recent_audio); - reg(new recent_keyframe); + reg(new recent_keyframes); reg(new recent_subtitle); - reg(new recent_timecode); + reg(new recent_timecodes); reg(new recent_video); /// @todo 16 is an implementation detail that maybe needs to be exposed for (int i = 0; i < 16; ++i) { reg(new mru_wrapper(i)); - reg(new mru_wrapper(i)); + reg(new mru_wrapper(i)); reg(new mru_wrapper(i)); - reg(new mru_wrapper(i)); + reg(new mru_wrapper(i)); reg(new mru_wrapper(i)); } } diff --git a/aegisub/src/frame_main.cpp b/aegisub/src/frame_main.cpp index 5b04d66de..388329918 100644 --- a/aegisub/src/frame_main.cpp +++ b/aegisub/src/frame_main.cpp @@ -144,10 +144,6 @@ FrameMain::FrameMain (wxArrayString args) context->previousFocus = 0; AegisubApp::Get()->frame = this; - StartupLog("Binding commands"); - // XXX: This is a hack for now, it will need to be dealt with when other frames are involved. - Bind(wxEVT_COMMAND_MENU_SELECTED, &FrameMain::cmd_call, this); - #ifdef __WXMAC__ // Bind(FrameMain::OnAbout, &FrameMain::cmd_call, this, cmd::id("app/about")); #endif @@ -163,7 +159,7 @@ FrameMain::FrameMain (wxArrayString args) InitToolbar(); StartupLog("Initialize menu bar"); - InitMenu(); + menu::GetMenuBar("main", this, context.get()); StartupLog("Create status bar"); CreateStatusBar(2); @@ -248,29 +244,26 @@ void FrameMain::cmd_call(wxCommandEvent& event) { LOG_D("event/select") << "Id: " << id; if (id < cmd::count()) cmd::call(context.get(), id); - else if (id >= ID_MENU_AUTOMATION_MACRO) - OnAutomationMacro(event); } +void FrameMain::OnMenuOpen(wxMenuEvent &event) { + if (wxMenu *menu = event.GetMenu()) { + wxMenuItemList& items = menu->GetMenuItems(); + for (wxMenuItemList::iterator it = items.begin(); it != items.end(); ++it) { + if (wxMenu *submenu = (*it)->GetSubMenu()) { + submenu->GetEventHandler()->QueueEvent(event.Clone()); + } + } + } +} + +/// @brief Initialize toolbar void FrameMain::InitToolbar () { wxSystemOptions::SetOption("msw.remap", 0); toolbar::AttachToolbar(this, "main", context.get(), "Default"); GetToolBar()->Realize(); } -void FrameMain::InitMenu() { - -#ifdef __WXMAC__ - // Make sure special menu items are placed correctly on Mac -// wxApp::s_macAboutMenuItemId = Menu_Help_About; -// wxApp::s_macExitMenuItemId = Menu_File_Exit; -// wxApp::s_macPreferencesMenuItemId = Menu_Tools_Options; -// wxApp::s_macHelpMenuTitleName = _("&Help"); -#endif - - SetMenuBar(menu::menu->GetMainMenu()); -} - void FrameMain::InitContents() { StartupLog("Create background panel"); Panel = new wxPanel(this,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL | wxCLIP_CHILDREN); @@ -607,7 +600,6 @@ BEGIN_EVENT_TABLE(FrameMain, wxFrame) EVT_SASH_DRAGGED(ID_SASH_MAIN_AUDIO, FrameMain::OnAudioBoxResize) - EVT_MENU_OPEN(FrameMain::OnMenuOpen) EVT_KEY_DOWN(FrameMain::OnKeyDown) #ifdef __WXMAC__ @@ -616,208 +608,6 @@ BEGIN_EVENT_TABLE(FrameMain, wxFrame) #endif END_EVENT_TABLE() -void FrameMain::RebuildRecentList(const char *root_command, const char *mru_name) { - wxMenu *menu = menu::menu->GetMenu(root_command); - - int count = (int)menu->GetMenuItemCount(); - for (int i=count;--i>=0;) { - menu->Destroy(menu->FindItemByPosition(i)); - } - - const agi::MRUManager::MRUListMap *map_list = config::mru->Get(mru_name); - if (map_list->empty()) { - menu->Append(-1, _("Empty"))->Enable(false); - return; - } - - int i = 0; - for (agi::MRUManager::MRUListMap::const_iterator it = map_list->begin(); it != map_list->end(); ++it) { - std::stringstream ss; - ss << root_command; - ss << "/"; - ss << i; - - wxFileName shortname(lagi_wxString(*it)); - - menu->Append(cmd::id(ss.str()), - wxString::Format("%s%d %s", i <= 9 ? "&" : "", i + 1, shortname.GetFullName())); - ++i; - } -} - -static void validate(wxMenuBar *menu, const agi::Context *c, const char *command) { - menu->Enable(cmd::id(command), cmd::get(command)->Validate(c)); -} - -static void check(wxMenuBar *menu, const agi::Context *c, const char *command) { - menu->Check(cmd::id(command), cmd::get(command)->IsActive(c)); -} - -void FrameMain::OnMenuOpen (wxMenuEvent &event) { - wxMenuBar *MenuBar = menu::menu->GetMainMenu(); - - MenuBar->Freeze(); - wxMenu *curMenu = event.GetMenu(); - - // File menu - if (curMenu == menu::menu->GetMenu("main/file")) { - RebuildRecentList("recent/subtitle", "Subtitle"); - validate(MenuBar, context.get(), "subtitle/open/video"); - } - - // View menu - else if (curMenu == menu::menu->GetMenu("main/view")) { - if (!showVideo && !showAudio) MenuBar->Check(cmd::id("app/display/subs"),true); - else if (showVideo && !showAudio) MenuBar->Check(cmd::id("app/display/video_subs"),true); - else if (showAudio && showVideo) MenuBar->Check(cmd::id("app/display/full"),true); - else MenuBar->Check(cmd::id("app/display/audio_subs"),true); - - check(MenuBar, context.get(), "grid/tags/show"); - check(MenuBar, context.get(), "grid/tags/simplify"); - check(MenuBar, context.get(), "grid/tags/hide"); - } - // Video menu - else if (curMenu == menu::menu->GetMenu("main/video")) { - validate(MenuBar, context.get(), "timecode/save"); - validate(MenuBar, context.get(), "timecode/close"); - validate(MenuBar, context.get(), "keyframe/close"); - validate(MenuBar, context.get(), "keyframe/save"); - - check(MenuBar, context.get(), "video/aspect/default"); - check(MenuBar, context.get(), "video/aspect/full"); - check(MenuBar, context.get(), "video/aspect/wide"); - check(MenuBar, context.get(), "video/aspect/cinematic"); - check(MenuBar, context.get(), "video/aspect/custom"); - - check(MenuBar, context.get(), "video/show_overscan"); - - RebuildRecentList("recent/video", "Video"); - RebuildRecentList("recent/timecode", "Timecodes"); - RebuildRecentList("recent/keyframe", "Keyframes"); - } - - // Audio menu - else if (curMenu == menu::menu->GetMenu("main/audio")) { - validate(MenuBar, context.get(), "audio/open/video"); - validate(MenuBar, context.get(), "audio/close"); - RebuildRecentList("recent/audio", "Audio"); - } - - // Subtitles menu - else if (curMenu == menu::menu->GetMenu("main/subtitle")) { - validate(MenuBar, context.get(), "main/subtitle/insert lines"); - validate(MenuBar, context.get(), "edit/line/duplicate"); - validate(MenuBar, context.get(), "edit/line/duplicate/shift"); - validate(MenuBar, context.get(), "edit/line/swap"); - validate(MenuBar, context.get(), "edit/line/join/concatenate"); - validate(MenuBar, context.get(), "edit/line/join/keep_first"); - validate(MenuBar, context.get(), "edit/line/join/as_karaoke"); - validate(MenuBar, context.get(), "main/subtitle/join lines"); - validate(MenuBar, context.get(), "edit/line/recombine"); - } - - // Timing menu - else if (curMenu == menu::menu->GetMenu("main/timing")) { - validate(MenuBar, context.get(), "time/snap/start_video"); - validate(MenuBar, context.get(), "time/snap/end_video"); - validate(MenuBar, context.get(), "time/snap/scene"); - validate(MenuBar, context.get(), "time/frame/current"); - - validate(MenuBar, context.get(), "time/continuous/start"); - validate(MenuBar, context.get(), "time/continuous/end"); - } - - // Edit menu - else if (curMenu == menu::menu->GetMenu("main/edit")) { - wxMenu *editMenu = menu::menu->GetMenu("main/edit"); - - // Undo state - wxString undo_text = wxString::Format("%s %s\t%s", - cmd::get("edit/undo")->StrMenu(), - context->ass->GetUndoDescription(), - hotkey::get_hotkey_str_first("Default", "edit/undo")); - wxMenuItem *item = editMenu->FindItem(cmd::id("edit/undo")); - item->SetItemLabel(undo_text); - item->Enable(!context->ass->IsUndoStackEmpty()); - - // Redo state - wxString redo_text = wxString::Format("%s %s\t%s", - cmd::get("edit/redo")->StrMenu(), - context->ass->GetRedoDescription(), - hotkey::get_hotkey_str_first("Default", "edit/redo")); - item = editMenu->FindItem(cmd::id("edit/redo")); - item->SetItemLabel(redo_text); - item->Enable(!context->ass->IsRedoStackEmpty()); - - validate(MenuBar, context.get(), "edit/line/cut"); - validate(MenuBar, context.get(), "edit/line/copy"); - validate(MenuBar, context.get(), "edit/line/paste"); - validate(MenuBar, context.get(), "edit/line/paste/over"); - } - - // Automation menu -#ifdef WITH_AUTOMATION - else if (curMenu == menu::menu->GetMenu("main/automation")) { - wxMenu *automationMenu = menu::menu->GetMenu("main/automation"); - - // Remove old macro items - for (unsigned int i = 0; i < activeMacroItems.size(); i++) { - wxMenu *p = 0; - wxMenuItem *it = MenuBar->FindItem(ID_MENU_AUTOMATION_MACRO + i, &p); - if (it) - p->Delete(it); - } - activeMacroItems.clear(); - - // Add new ones - int added = 0; - added += AddMacroMenuItems(automationMenu, wxGetApp().global_scripts->GetMacros()); - added += AddMacroMenuItems(automationMenu, context->local_scripts->GetMacros()); - - // If none were added, show a ghosted notice - if (added == 0) { - automationMenu->Append(ID_MENU_AUTOMATION_MACRO, _("No Automation macros loaded"))->Enable(false); - activeMacroItems.push_back(0); - } - } -#endif - - MenuBar->Thaw(); -} - -int FrameMain::AddMacroMenuItems(wxMenu *menu, const std::vector ¯os) { -#ifdef WITH_AUTOMATION - if (macros.empty()) { - return 0; - } - - int id = activeMacroItems.size();; - for (std::vector::const_iterator i = macros.begin(); i != macros.end(); ++i) { - wxMenuItem * m = menu->Append(ID_MENU_AUTOMATION_MACRO + id, (*i)->GetName(), (*i)->GetDescription()); - m->Enable((*i)->Validate(context->ass, SubsGrid->GetAbsoluteSelection(), SubsGrid->GetFirstSelRow())); - activeMacroItems.push_back(*i); - id++; - } - - return macros.size(); -#else - return 0; -#endif -} - -void FrameMain::OnAutomationMacro (wxCommandEvent &event) { -#ifdef WITH_AUTOMATION - SubsGrid->BeginBatch(); - // First get selection data - std::vector selected_lines = SubsGrid->GetAbsoluteSelection(); - int first_sel = SubsGrid->GetFirstSelRow(); - // Run the macro... - activeMacroItems[event.GetId()-ID_MENU_AUTOMATION_MACRO]->Process(context->ass, selected_lines, first_sel, this); - SubsGrid->SetSelectionFromAbsolute(selected_lines); - SubsGrid->EndBatch(); -#endif -} - void FrameMain::OnCloseWindow (wxCloseEvent &event) { // Stop audio and video context->videoController->Stop(); diff --git a/aegisub/src/frame_main.h b/aegisub/src/frame_main.h index 4b82e194f..133099f49 100644 --- a/aegisub/src/frame_main.h +++ b/aegisub/src/frame_main.h @@ -99,7 +99,6 @@ class FrameMain: public wxFrame { void InitToolbar(); void InitContents(); - void InitMenu(); bool LoadList(wxArrayString list); void UpdateTitle(); @@ -112,15 +111,11 @@ class FrameMain: public wxFrame { void OnAutoSave(wxTimerEvent &event); void OnStatusClear(wxTimerEvent &event); void OnCloseWindow (wxCloseEvent &event); - /// @brief General handler for all Automation-generated menu items - void OnAutomationMacro(wxCommandEvent &event); /// Close the currently open subs, asking the user if they want to save if there are unsaved changes /// @param enableCancel Should the user be able to cancel the close? int TryToCloseSubs(bool enableCancel=true); - void RebuildRecentList(const char *root_command, const char *mru_name); - // AudioControllerAudioEventListener implementation void OnAudioOpen(AudioProvider *provider); void OnAudioClose(); diff --git a/aegisub/src/include/aegisub/menu.h b/aegisub/src/include/aegisub/menu.h index d60b77ba3..4cb362b9b 100644 --- a/aegisub/src/include/aegisub/menu.h +++ b/aegisub/src/include/aegisub/menu.h @@ -1,4 +1,4 @@ -// Copyright (c) 2010, Amar Takhar +// Copyright (c) 2011, Thomas Goyne // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -19,51 +19,44 @@ /// @ingroup menu toolbar #ifndef AGI_PRE -#include - -#include +#include #endif #include -namespace json { class Array; } +namespace agi { struct Context; } + +class wxMenu; +class wxMenuBar; namespace menu { -DEFINE_BASE_EXCEPTION_NOINNER(MenuError, agi::Exception) -DEFINE_SIMPLE_EXCEPTION_NOINNER(MenuJsonValueArray, MenuError, "menu/value/array") -DEFINE_SIMPLE_EXCEPTION_NOINNER(MenuJsonValueSingle, MenuError, "menu/value") -DEFINE_SIMPLE_EXCEPTION_NOINNER(MenuJsonValueNull, MenuError, "menu/value") -DEFINE_SIMPLE_EXCEPTION_NOINNER(MenuInvalidName, MenuError, "menu/invalid") + DEFINE_BASE_EXCEPTION_NOINNER(Error, agi::Exception) + DEFINE_SIMPLE_EXCEPTION_NOINNER(UnknownMenu, Error, "menu/unknown") + DEFINE_SIMPLE_EXCEPTION_NOINNER(InvalidMenu, Error, "menu/invalid") -class Menu; -extern Menu *menu; + /// @brief Get the menu with the specified name as a wxMenuBar + /// @param name Name of the menu + /// + /// Throws: + /// UnknownMenu if no menu with the given name was found + /// BadMenu if there is a menu with the given name, but it is invalid + void GetMenuBar(std::string const& name, wxFrame *window, agi::Context *c); -class Menu { -public: - Menu(); - ~Menu(); - wxMenuBar* GetMainMenu() { return main_menu; } - wxMenu* GetMenu(std::string name); + /// @brief Get the menu with the specified name as a wxMenu + /// @param name Name of the menu + /// + /// Throws: + /// UnknownMenu if no menu with the given name was found + /// BadMenu if there is a menu with the given name, but it is invalid + wxMenu *GetMenu(std::string const& name, agi::Context *c); -private: - - typedef std::map MTMap; - typedef std::pair MTPair; - - enum MenuTypes { - Option = 1, - Check = 2, - Radio = 3, - Submenu = 4, - Recent = 5, - Spacer = 100 - }; - - wxMenuBar *main_menu; - MTMap map; - - wxMenu* BuildMenu(std::string name, const json::Array& array, int submenu=0); - -}; - -} // namespace menu + /// @brief Open a popup menu at the mouse + /// @param menu Menu to open + /// @param parent_window Parent window for the menu; cannot be NULL + /// + /// This function should be used rather than wxWindow::PopupMenu due to + /// that PopupMenu does not trigger menu open events and triggers update + /// ui events on the opening window rather than the menu for some bizarre + /// reason + void OpenPopupMenu(wxMenu *menu, wxWindow *parent_window); +} diff --git a/aegisub/src/libresrc/default_menu.json b/aegisub/src/libresrc/default_menu.json index b23c624f6..5bcbb65a2 100644 --- a/aegisub/src/libresrc/default_menu.json +++ b/aegisub/src/libresrc/default_menu.json @@ -1,580 +1,175 @@ { - "main" : [ - { - "type" : 4, - "command" : "file", - "contents" : [ - { - "type" : 1, - "command" : "subtitle/new" - }, - { - "type" : 1, - "command" : "subtitle/open" - }, - { - "type" : 1, - "command" : "subtitle/open/charset" - }, - { - "type" : 1, - "command" : "subtitle/open/video" - }, - { - "type" : 1, - "command" : "subtitle/save" - }, - { - "type" : 1, - "command" : "subtitle/save/as" - }, - { - "type" : 1, - "command" : "tool/export" - }, - { - "type" : 5, - "command" : "recent/subtitle" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "subtitle/properties" - }, - { - "type" : 1, - "command" : "subtitle/attachment" - }, - { - "type" : 1, - "command" : "tool/font_collector" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "app/new_window" - }, - { - "type" : 1, - "command" : "app/exit" - } - ] - }, - { - "type" : 4, - "command" : "edit", - "contents" : [ - { - "type" : 1, - "command" : "edit/undo" - }, - { - "type" : 1, - "command" : "edit/redo" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "edit/line/cut" - }, - { - "type" : 1, - "command" : "edit/line/copy" - }, - { - "type" : 1, - "command" : "edit/line/paste" - }, - { - "type" : 1, - "command" : "edit/line/paste/over" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "subtitle/find" - }, - { - "type" : 1, - "command" : "subtitle/find/next" - }, - { - "type" : 1, - "command" : "edit/search_replace" - } - ] - }, - { - "type" : 4, - "command" : "subtitle", - "contents" : [ - { - "type" : 1, - "command" : "tool/style/manager" - }, - { - "type" : 1, - "command" : "tool/style/assistant" - }, - { - "type" : 1, - "command" : "tool/translation_assistant" - }, - { - "type" : 1, - "command" : "tool/resampleres" - }, - { - "type" : 1, - "command" : "subtitle/spellcheck" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "tool/assdraw" - }, - { - "type" : 4, - "command" : "insert lines", - "contents" : [ - { - "type" : 1, - "command" : "subtitle/insert/before" - }, - { - "type" : 1, - "command" : "subtitle/insert/after" - }, - { - "type" : 1, - "command" : "subtitle/insert/before/videotime" - }, - { - "type" : 1, - "command" : "subtitle/insert/after/videotime" - } - ] - }, - { - "type" : 1, - "command" : "edit/line/duplicate" - }, - { - "type" : 1, - "command" : "edit/line/duplicate/shift" - }, - { - "type" : 1, - "command" : "edit/line/delete" - }, - { - "type" : 100 - }, - { - "type" : 4, - "command" : "join lines", - "contents" : [ - { - "type" : 1, - "command" : "edit/line/join/concatenate" - }, - { - "type" : 1, - "command" : "edit/line/join/keep_first" - }, - { - "type" : 1, - "command" : "edit/line/join/as_karaoke" - } - ] - }, - { - "type" : 1, - "command" : "edit/line/recombine" - }, - { - "type" : 1, - "command" : "edit/line/split/by_karaoke" - }, - { - "type" : 100 - }, - { - "type" : 4, - "command" : "sort lines", - "contents" : [ - { - "type" : 1, - "command" : "time/sort/start" - }, - { - "type" : 1, - "command" : "time/sort/end" - }, - { - "type" : 1, - "command" : "time/sort/style" - } - ] - }, - { - "type" : 1, - "command" : "edit/line/swap" - }, - { - "type" : 1, - "command" : "tool/line/select" - } - ] - }, - { - "type" : 4, - "command" : "timing", - "contents" : [ - { - "type" : 1, - "command" : "time/shift" - }, - { - "type" : 1, - "command" : "tool/time/postprocess" - }, - { - "type" : 1, - "command" : "tool/time/kanji" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "time/snap/start_video" - }, - { - "type" : 1, - "command" : "time/snap/end_video" - }, - { - "type" : 1, - "command" : "time/snap/scene" - }, - { - "type" : 1, - "command" : "time/frame/current" - }, - { - "type" : 100 - }, - { - "type" : 4, - "command" : "make times continuous", - "contents" : [ - { - "type" : 1, - "command" : "time/continuous/start" - }, - { - "type" : 1, - "command" : "time/continuous/end" - } - ] - } - ] - }, - - { - "type" : 4, - "command" : "video", - "contents" : [ - { - "type" : 1, - "command" : "video/open" - }, - { - "type" : 1, - "command" : "video/close" - }, - { - "type" : 5, - "command" : "recent/video" - }, - { - "type" : 1, - "command" : "video/open/dummy" - }, - { - "type" : 1, - "command" : "video/details" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "timecode/open" - }, - { - "type" : 1, - "command" : "timecode/save" - }, - { - "type" : 1, - "command" : "timecode/close" - }, - { - "type" : 5, - "command" : "recent/timecode" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "keyframe/open" - }, - { - "type" : 1, - "command" : "keyframe/save" - }, - { - "type" : 1, - "command" : "keyframe/close" - }, - { - "type" : 5, - "command" : "recent/keyframe" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "video/detach" - }, - { - "type" : 4, - "command" : "set zoom", - "contents" : [ - { - "type" : 1, - "command" : "video/zoom/50" - }, - { - "type" : 1, - "command" : "video/zoom/100" - }, - { - "type" : 1, - "command" : "video/zoom/200" - } - ] - }, - { - "type" : 4, - "command" : "override ar", - "contents" : [ - { - "type" : 2, - "command" : "video/aspect/default" - }, - { - "type" : 2, - "command" : "video/aspect/full" - }, - { - "type" : 2, - "command" : "video/aspect/wide" - }, - { - "type" : 2, - "command" : "video/aspect/cinematic" - }, - { - "type" : 2, - "command" : "video/aspect/custom" - } - ] - }, - { - "type" : 2, - "command" : "video/show_overscan" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "video/jump" - }, - { - "type" : 1, - "command" : "video/jump/start" - }, - { - "type" : 1, - "command" : "video/jump/end" - } - ] - }, - { - "type" : 4, - "command" : "audio", - "contents" : [ - { - "type" : 1, - "command" : "audio/open" - }, - { - "type" : 1, - "command" : "audio/open/video" - }, - { - "type" : 1, - "command" : "audio/close" - }, - { - "type" : 5, - "command" : "recent/audio" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "audio/view/spectrum" - }, - { - "type" : 1, - "command" : "audio/view/waveform" - }, - { - "type" : 1, - "command" : "audio/open/blank" - }, - { - "type" : 1, - "command" : "audio/open/noise" - } - ] - }, - { - "type" : 4, - "command" : "automation", - "contents" : [ - { - "type" : 1, - "command" : "am/manager" - }, - { - "type" : 100 - } - ] - }, - { - "type" : 4, - "command" : "view", - "contents" : [ - { - "type" : 1, - "command" : "app/language" - }, - { - "type" : 1, - "command" : "app/options" - }, - { - "type" : 100 - }, - { - "type" : 3, - "command" : "app/display/subs" - }, - { - "type" : 3, - "command" : "app/display/video_subs" - }, - { - "type" : 3, - "command" : "app/display/audio_subs" - }, - { - "type" : 3, - "command" : "app/display/full" - }, - { - "type" : 100 - }, - { - "type" : 3, - "command" : "grid/tags/show" - }, - { - "type" : 3, - "command" : "grid/tags/simplify" - }, - { - "type" : 3, - "command" : "grid/tags/hide" - } - ] - }, - { - "type" : 4, - "command" : "help", - "contents" : [ - { - "type" : 1, - "command" : "help/contents" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "help/files" - }, - { - "type" : 1, - "command" : "help/website" - }, - { - "type" : 1, - "command" : "help/forums" - }, - { - "type" : 1, - "command" : "help/bugs" - }, - { - "type" : 100 - }, - { - "type" : 1, - "command" : "help/irc" - }, - { - "type" : 1, - "command" : "app/updates" - }, - { - "type" : 1, - "command" : "app/about" - }, - { - "type" : 1, - "command" : "app/log" - } - ] - } - ] + "main" : [ + { "submenu" : "main/file", "text" : "&File" }, + { "submenu" : "main/edit", "text" : "&Edit" }, + { "submenu" : "main/subtitle", "text" : "&Subtitle" }, + { "submenu" : "main/timing", "text" : "&Timing" }, + { "submenu" : "main/video", "text" : "&Video" }, + { "submenu" : "main/audio", "text" : "&Audio" }, + { "submenu" : "main/automation", "text" : "A&utomation" }, + { "submenu" : "main/view", "text" : "&View" }, + { "submenu" : "main/help", "text" : "&Help", "special" : "help" } + ], + "main/file" : [ + { "command" : "subtitle/new" }, + { "command" : "subtitle/open" }, + { "command" : "subtitle/open/charset" }, + { "command" : "subtitle/open/video" }, + { "command" : "subtitle/save" }, + { "command" : "subtitle/save/as" }, + { "command" : "tool/export" }, + { "recent" : "Subtitle" }, + {}, + { "command" : "subtitle/properties" }, + { "command" : "subtitle/attachment" }, + { "command" : "tool/font_collector" }, + {}, + { "command" : "app/new_window" }, + { "command" : "app/exit", "special" : "exit" } + ], + "main/edit" : [ + { "command" : "edit/undo" }, + { "command" : "edit/redo" }, + {}, + { "command" : "edit/line/cut" }, + { "command" : "edit/line/copy" }, + { "command" : "edit/line/paste" }, + { "command" : "edit/line/paste/over" }, + {}, + { "command" : "subtitle/find" }, + { "command" : "subtitle/find/next" }, + { "command" : "edit/search_replace" } + ], + "main/subtitle" : [ + { "command" : "tool/style/manager" }, + { "command" : "tool/style/assistant" }, + { "command" : "tool/translation_assistant" }, + { "command" : "tool/resampleres" }, + { "command" : "subtitle/spellcheck" }, + {}, + { "command" : "tool/assdraw" }, + { "submenu" : "main/subtitle/insert lines", "text" : "&Insert Lines" }, + { "command" : "edit/line/duplicate" }, + { "command" : "edit/line/duplicate/shift" }, + { "command" : "edit/line/delete" }, + {}, + { "submenu" : "main/subtitle/join lines", "text" : "Join Lines" }, + { "command" : "edit/line/recombine" }, + { "command" : "edit/line/split/by_karaoke" }, + {}, + { "submenu" : "main/subtitle/sort lines", "text" : "Sort Lines" }, + { "command" : "edit/line/swap" }, + { "command" : "tool/line/select" } + ], + "main/subtitle/insert lines" : [ + { "command" : "subtitle/insert/before" }, + { "command" : "subtitle/insert/after" }, + { "command" : "subtitle/insert/before/videotime" }, + { "command" : "subtitle/insert/after/videotime" } + ], + "main/subtitle/join lines" : [ + { "command" : "edit/line/join/concatenate" }, + { "command" : "edit/line/join/keep_first" }, + { "command" : "edit/line/join/as_karaoke" } + ], + "main/subtitle/sort lines" : [ + { "command" : "time/sort/start" }, + { "command" : "time/sort/end" }, + { "command" : "time/sort/style" } + ], + "main/timing" : [ + { "command" : "time/shift" }, + { "command" : "tool/time/postprocess" }, + { "command" : "tool/time/kanji" }, + {}, + { "command" : "time/snap/start_video" }, + { "command" : "time/snap/end_video" }, + { "command" : "time/snap/scene" }, + { "command" : "time/frame/current" }, + {}, + { "submenu" : "main/timing/make times continuous", "text" : "Make Times Continuous" } + ], + "main/timing/make times continuous" : [ + { "command" : "time/continuous/start" }, + { "command" : "time/continuous/end" } + ], + "main/video" : [ + { "command" : "video/open" }, + { "command" : "video/close" }, + { "recent" : "Video" }, + { "command" : "video/open/dummy" }, + { "command" : "video/details" }, + {}, + { "command" : "timecode/open" }, + { "command" : "timecode/save" }, + { "command" : "timecode/close" }, + { "recent" : "Timecodes" }, + {}, + { "command" : "keyframe/open" }, + { "command" : "keyframe/save" }, + { "command" : "keyframe/close" }, + { "recent" : "Keyframes" }, + {}, + { "command" : "video/detach" }, + { "submenu" : "main/video/set zoom", "text" : "Set Zoom" }, + { "submenu" : "main/video/override ar", "text" : "Override AR" }, + { "command" : "video/show_overscan" }, + {}, + { "command" : "video/jump" }, + { "command" : "video/jump/start" }, + { "command" : "video/jump/end" } + ], + "main/video/set zoom" : [ + { "command" : "video/zoom/50" }, + { "command" : "video/zoom/100" }, + { "command" : "video/zoom/200" } + ], + "main/video/override ar" : [ + { "command" : "video/aspect/default" }, + { "command" : "video/aspect/full" }, + { "command" : "video/aspect/wide" }, + { "command" : "video/aspect/cinematic" }, + { "command" : "video/aspect/custom" } + ], + "main/audio" : [ + { "command" : "audio/open" }, + { "command" : "audio/open/video" }, + { "command" : "audio/close" }, + { "recent" : "Audio" }, + {}, + { "command" : "audio/view/spectrum" }, + { "command" : "audio/view/waveform" }, + { "command" : "audio/open/blank" }, + { "command" : "audio/open/noise" } + ], + "main/automation" : [ + { "command" : "am/manager" }, + {} + ], + "main/view" : [ + { "command" : "app/language" }, + { "command" : "app/options", "special" : "options" }, + {}, + { "command" : "app/display/subs" }, + { "command" : "app/display/video_subs" }, + { "command" : "app/display/audio_subs" }, + { "command" : "app/display/full" }, + {}, + { "command" : "grid/tags/show" }, + { "command" : "grid/tags/simplify" }, + { "command" : "grid/tags/hide" } + ], + "main/help" : [ + { "command" : "help/contents" }, + {}, + { "command" : "help/files" }, + { "command" : "help/website" }, + { "command" : "help/forums" }, + { "command" : "help/bugs" }, + {}, + { "command" : "help/irc" }, + { "command" : "app/updates" }, + { "command" : "app/about", "special" : "about" }, + { "command" : "app/log" } + ] } diff --git a/aegisub/src/main.cpp b/aegisub/src/main.cpp index 8450267d7..155a8c55c 100644 --- a/aegisub/src/main.cpp +++ b/aegisub/src/main.cpp @@ -195,9 +195,6 @@ bool AegisubApp::OnInit() { // Init icons. icon::icon_init(); - // Generate menus. - menu::menu = new menu::Menu(); - // Install assertion handler // wxSetAssertHandler(wxAssertHandler); @@ -312,10 +309,12 @@ bool AegisubApp::OnInit() { return false; } +#ifndef _DEBUG catch (...) { wxMessageBox(_T("Unhandled exception"),_T("Fatal error while initializing")); return false; } +#endif StartupLog(_T("Initialization complete")); return true; @@ -334,7 +333,6 @@ int AegisubApp::OnExit() { delete config::mru; delete agi::hotkey::hotkey; delete config::path; - delete menu::menu; cmd::clear(); #ifdef WITH_AUTOMATION diff --git a/aegisub/src/menu.cpp b/aegisub/src/menu.cpp index cd8806549..4801924bb 100644 --- a/aegisub/src/menu.cpp +++ b/aegisub/src/menu.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2010, Amar Takhar +// Copyright (c) 2011, Thomas Goyne // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -20,144 +20,339 @@ #include "config.h" -#ifndef AGI_PRE -#include - -#include - -#include -#endif - #include "include/aegisub/menu.h" -#include +#include "include/aegisub/hotkey.h" + +#include "command/command.h" +#include "compat.h" +#include "libresrc/libresrc.h" +#include "main.h" +#include "standard_paths.h" + #include #include -#include "include/aegisub/hotkey.h" -#include "command/command.h" -#include "libresrc/libresrc.h" -#include "main.h" +#ifndef AGI_PRE +#include +#include +#include -namespace menu { +#include +#include +#endif -menu::Menu *menu; +namespace { +/// Window ID of first menu item +static const int MENU_ID_BASE = 10000; -Menu::Menu() { +using std::tr1::placeholders::_1; +using std::tr1::bind; - main_menu = new wxMenuBar(); +/// Get the menu text for a command along with hotkey +inline wxString get_menu_text(cmd::Command *command, agi::Context *c) { + return command->StrMenu(c) + "\t" + hotkey::get_hotkey_str_first("Default", command->name()); +} - std::istringstream *stream = new std::istringstream(GET_DEFAULT_CONFIG(default_menu)); - json::UnknownElement menu_root = agi::json_util::parse(stream); +class MruMenu : public wxMenu { + std::string type; + std::vector items; + std::vector *cmds; - LOG_D("menu/init") << "Generating Menus"; - json::Object object = menu_root; - - for (json::Object::const_iterator index(object.Begin()); index != object.End(); index++) { - const json::Object::Member& member = *index; - const json::Array& array = member.element; - delete BuildMenu(member.name, array); + void Resize(size_t new_size) { + for (size_t i = GetMenuItemCount(); i > new_size; --i) { + Remove(FindItemByPosition(i - 1)); } + + for (size_t i = GetMenuItemCount(); i < new_size; ++i) { + if (i >= items.size()) { + items.push_back(new wxMenuItem(this, MENU_ID_BASE + cmds->size(), "_")); + cmds->push_back(cmd::get(STD_STR(wxString::Format("recent/%s/%d", lagi_wxString(type).Lower(), i)))); } + Append(items[i]); + } + } -Menu::~Menu() {} +public: + MruMenu(std::string const& type, std::vector *cmds) + : type(type) + , cmds(cmds) + { + } + ~MruMenu() { + // Append all items to ensure that they're all cleaned up + Resize(items.size()); + } -wxMenu* Menu::GetMenu(std::string name) { + void Update() { + const agi::MRUManager::MRUListMap *mru = config::mru->Get(type); - MTMap::iterator index; + Resize(mru->size()); - if ((index = map.find(name)) != map.end()) { - return index->second; + if (mru->empty()) { + Resize(1); + items[0]->Enable(false); + items[0]->SetItemLabel(_("Empty")); + return; } - LOG_E("menu/invalid") << "Invalid index name: " << name; - throw MenuInvalidName("Unknown index"); + int i = 0; + for (agi::MRUManager::MRUListMap::const_iterator it = mru->begin(); + it != mru->end(); + ++it, ++i) + { + items[i]->SetItemLabel(wxString::Format("%s%d %s", + i <= 9 ? "&" : "", i + 1, + wxFileName(lagi_wxString(*it)).GetFullName())); + items[i]->Enable(true); + } + } +}; + +/// @class CommandManager +/// @brief Event dispatcher to update menus on open and handle click events +/// +/// Some of what the class does could be dumped off on wx, but wxEVT_MENU_OPEN +/// is super buggy (GetMenu() often returns NULL and it outright doesn't trigger +/// on submenus in many cases, and registering large numbers of wxEVT_UPDATE_UI +/// handlers makes everything involves events unusably slow. +class CommandManager { + /// Menu items which need to do something on menu open + std::deque > dynamic_items; + /// window id -> command map + std::vector items; + /// MRU menus which need to be updated on menu open + std::deque mru; + + /// Project context + agi::Context *context; + + /// Update a single dynamic menu item + void UpdateItem(std::pair const& item) { + int flags = item.first->Type(); + if (flags & cmd::COMMAND_DYNAMIC_NAME) + item.second->SetItemLabel(get_menu_text(item.first, context)); + if (flags & cmd::COMMAND_VALIDATE) + item.second->Enable(item.first->Validate(context)); + if (flags & cmd::COMMAND_RADIO || flags & cmd::COMMAND_TOGGLE) + item.second->Check(item.first->IsActive(context)); } +public: + CommandManager(agi::Context *context) : context(context) { } + /// Append a command to a menu and register the needed handlers + int AddCommand(cmd::Command *co, wxMenu *parent, std::string const& text) { + int flags = co->Type(); + wxItemKind kind = + flags & cmd::COMMAND_RADIO ? wxITEM_RADIO : + flags & cmd::COMMAND_TOGGLE ? wxITEM_CHECK : + wxITEM_NORMAL; + wxString menu_text = text.empty() ? + get_menu_text(co, context) : + lagi_wxString(text) + "\t" + hotkey::get_hotkey_str_first("Default", co->name()); -wxMenu* Menu::BuildMenu(std::string name, const json::Array& array, int submenu) { - wxMenu *menu = new wxMenu(); + wxMenuItem *item = new wxMenuItem(parent, MENU_ID_BASE + items.size(), menu_text, co->StrHelp(), kind); +#ifndef __WXMAC__ + /// @todo Maybe make this a configuration option instead? + item->SetBitmap(co->Icon(16)); +#endif + parent->Append(item); + items.push_back(co); + if (flags != cmd::COMMAND_NORMAL) + dynamic_items.push_back(std::make_pair(co, item)); - for (json::Array::const_iterator index(array.Begin()); index != array.End(); index++) { - std::string name_sub = name; - - const json::Object& obj = *index; - const json::Number& type_number = obj["type"]; - int type = type_number.Value(); - - if (type == Menu::Spacer) { - menu->AppendSeparator(); - continue; + return item->GetId(); } + /// Create a MRU menu and register the needed handlers + /// @param name MRU type + /// @param parent Menu to append the new MRU menu to + void AddRecent(std::string const& name, wxMenu *parent) { + mru.push_back(new MruMenu(name, &items)); + parent->AppendSubMenu(mru.back(), _("Recent")); + } - const json::String& command = obj["command"]; - std::string name_submenu = name_sub + "/" + command.Value(); + void OnMenuOpen(wxMenuEvent &) { + for_each(dynamic_items.begin(), dynamic_items.end(), bind(&CommandManager::UpdateItem, this, _1)); + for_each(mru.begin(), mru.end(), std::mem_fun(&MruMenu::Update)); + } + void OnMenuClick(wxCommandEvent &evt) { + // This also gets clicks on unrelated things such as the toolbar, so + // the window ID ranges really need to be unique + size_t id = static_cast(evt.GetId() - MENU_ID_BASE); + if (id < items.size()) + (*items[id])(context); + } +}; + +/// Wrapper for wxMenu to add a command manager +struct CommandMenu : public wxMenu { + CommandManager cm; + CommandMenu(agi::Context *c) : cm(c) { } +}; + +/// Wrapper for wxMenuBar to add a command manager +struct CommandMenuBar : public wxMenuBar { + CommandManager cm; + CommandMenuBar(agi::Context *c) : cm(c) { } +}; + +/// Read a string from a json object +/// @param obj Object to read from +/// @param name Index to read from +/// @param[out] value Output value to write to +/// @return Was the requested index found +bool read_entry(json::Object const& obj, const char *name, std::string *value) { + json::Object::const_iterator it = obj.Find(name); + if (it == obj.End()) return false; + *value = static_cast(it->element); + return true; +} + +typedef json::Array menu_items; +typedef json::Object menu_map; + +/// Get the root object of the menu configuration +menu_map const& get_menus_root() { + static menu_map root; + if (!root.Empty()) return root; - std::string cmd_name = type == Menu::Submenu ? name_submenu : command.Value(); - cmd::Command *cmd; try { - cmd = cmd::get(cmd_name); + root = agi::json_util::file(StandardPaths::DecodePath("?user/menu.json").utf8_str().data(), GET_DEFAULT_CONFIG(default_menu)); + return root; } - catch (CommandNotFound const&) { - LOG_W("menu/command/not_found") << "Command '" << cmd_name << "' not found; skipping"; - continue; + catch (json::Reader::ParseException const& e) { + LOG_E("menu/parse") << "json::ParseException: " << e.what() << ", Line/offset: " << e.m_locTokenBegin.m_nLine + 1 << '/' << e.m_locTokenBegin.m_nLineOffset + 1; + throw; } - - wxString display = cmd->StrMenu() + "\t" + hotkey::get_hotkey_str_first("Default", cmd_name); - wxString descr = cmd->StrHelp(); - - switch (type) { - case Menu::Option: { - wxMenuItem *menu_item = new wxMenuItem(menu, cmd::id(command.Value()), display, descr, wxITEM_NORMAL); - menu->Append(menu_item); + catch (std::exception const& e) { + LOG_E("menu/parse") << e.what(); + throw; } - break; - - case Menu::Check: { - menu->AppendCheckItem(cmd::id(command.Value()), display, descr); } - break; - case Menu::Radio: { - menu->AppendRadioItem(cmd::id(command.Value()), display, descr); +/// Get the menu with the specified name +/// @param name Name of menu to get +/// @return Array of menu items +menu_items const& get_menu(std::string const& name) { + menu_map const& root = get_menus_root(); + + menu_map::const_iterator it = root.Find(name); + if (it == root.End()) throw menu::UnknownMenu("Menu named " + name + " not found"); + return it->element; +} + +wxMenu *build_menu(std::string const& name, agi::Context *c, CommandManager *cm, wxMenu *menu = 0); + +/// Recursively process a single entry in the menu json +/// @param parent Menu to add the item(s) from this entry to +/// @param c Project context to bind the menu to +/// @param ele json object to process +/// @param cm Command manager for this menu +void process_menu_item(wxMenu *parent, agi::Context *c, json::Object const& ele, CommandManager *cm) { + if (ele.Empty()) { + parent->AppendSeparator(); + return; } - break; - case Menu::Recent: { - wxMenu *menu_new = new wxMenu(); - wxMenuItem *menu_item = new wxMenuItem(menu, cmd::id(command.Value()), display, descr, wxITEM_NORMAL, menu_new); - menu->Append(menu_item); - map.insert(MTPair(command.Value(), menu_new)); + std::string submenu, recent, command, text, special; + read_entry(ele, "special", &special); + if (read_entry(ele, "submenu", &submenu) && read_entry(ele, "text", &text)) { + parent->AppendSubMenu(build_menu(submenu, c, cm), lagi_wxString(text)); +#ifdef __WXMAC__ + if (special == "help") + wxApp::s_macHelpMenuTitleName = lagi_wxString(text); +#endif + return; } - break; - case Menu::Submenu: { + if (read_entry(ele, "recent", &recent)) { + cm->AddRecent(recent, parent); + return; + } - const json::Array& arr = obj["contents"]; - - wxMenu *menu_new = BuildMenu(name_sub.append("/").append(command), arr, 1); - map.insert(MTPair(name_submenu, menu_new)); - - if (submenu) { - wxMenuItem *menu_item = new wxMenuItem(menu, cmd::id(name_sub), display, descr, wxITEM_NORMAL, menu_new); - menu->Append(menu_item); - } else { - main_menu->Append(menu_new, display); + if (special == "automation") { +#ifdef WITH_AUTOMATION + /// @todo Actually implement this + parent->Append(-1, _("No Automation macros loaded"))->Enable(false); +#endif + return; } + + if (!read_entry(ele, "command", &command)) + return; + + read_entry(ele, "text", &text); + + try { + int id = cm->AddCommand(cmd::get(command), parent, text); +#ifdef __WXMAC__ + if (!special.empty()) { + if (special == "about") + wxApp::s_macAboutMenuItemId = id; + else if (special == "exit") + wxApp::s_macExitMenuItemId = id; + else if (special == "options") + wxApp::s_macPreferencesMenuItemId = id; + } +#endif + } + catch (agi::Exception const& e) { +#ifdef _DEBUG + parent->Append(-1, lagi_wxString(e.GetMessage()))->Enable(false); +#endif + LOG_W("menu/command/not_found") << "Skipping command " << command << ": " << e.GetMessage(); } - break; } - } // for index +/// Build the menu with the given name +/// @param name Name of the menu +/// @param c Project context to bind the menu to +wxMenu *build_menu(std::string const& name, agi::Context *c, CommandManager *cm, wxMenu *menu) { + menu_items const& items = get_menu(name); + if (!menu) menu = new wxMenu; + for_each(items.Begin(), items.End(), bind(process_menu_item, menu, c, _1, cm)); return menu; } -} // namespace menu +} + +namespace menu { + void GetMenuBar(std::string const& name, wxFrame *window, agi::Context *c) { + menu_items const& items = get_menu(name); + + CommandMenuBar *menu = new CommandMenuBar(c); + for (menu_items::const_iterator it = items.Begin(); it != items.End(); ++it) { + std::string submenu, disp; + read_entry(*it, "submenu", &submenu); + read_entry(*it, "text", &disp); + menu->Append(build_menu(submenu, c, &menu->cm), lagi_wxString(disp)); + } + + window->SetMenuBar(menu); + window->Bind(wxEVT_MENU_OPEN, &CommandManager::OnMenuOpen, &menu->cm); + window->Bind(wxEVT_COMMAND_MENU_SELECTED, &CommandManager::OnMenuClick, &menu->cm); + } + + wxMenu *GetMenu(std::string const& name, agi::Context *c) { + CommandMenu *menu = new CommandMenu(c); + build_menu(name, c, &menu->cm, menu); + menu->Bind(wxEVT_MENU_OPEN, &CommandManager::OnMenuOpen, &menu->cm); + menu->Bind(wxEVT_COMMAND_MENU_SELECTED, &CommandManager::OnMenuClick, &menu->cm); + return menu; + } + + void OpenPopupMenu(wxMenu *menu, wxWindow *parent_window) { + wxMenuEvent evt; + evt.SetEventType(wxEVT_MENU_OPEN); + menu->ProcessEvent(evt); + parent_window->PopupMenu(menu); + } +} diff --git a/aegisub/src/subs_grid.cpp b/aegisub/src/subs_grid.cpp index c7689ed17..cd160f270 100644 --- a/aegisub/src/subs_grid.cpp +++ b/aegisub/src/subs_grid.cpp @@ -134,7 +134,7 @@ void SubtitlesGrid::OnCommand(wxCommandEvent& event) { static inline void append_command(wxMenu &menu, const char *name, const agi::Context *context) { cmd::Command *c = cmd::get(name); - menu.Append(cmd::id(name), c->StrMenu(), c->StrHelp())->Enable(c->Validate(context)); + menu.Append(cmd::id(name), c->StrMenu(context), c->StrHelp())->Enable(c->Validate(context)); } /// @brief Popup menu diff --git a/aegisub/src/toolbar.cpp b/aegisub/src/toolbar.cpp index 4e64f404d..3a67769ff 100644 --- a/aegisub/src/toolbar.cpp +++ b/aegisub/src/toolbar.cpp @@ -143,7 +143,7 @@ namespace { } if (hotkeys.size()) tooltip += ")"; - AddTool(TOOL_ID_BASE + commands.size(), command->StrDisplay(), icon, tooltip, kind); + AddTool(TOOL_ID_BASE + commands.size(), command->StrDisplay(context), icon, tooltip, kind); commands.push_back(command); needs_onidle = needs_onidle || flags != cmd::COMMAND_NORMAL; diff --git a/aegisub/src/utils.h b/aegisub/src/utils.h index 2c54fad17..67e6b02bd 100644 --- a/aegisub/src/utils.h +++ b/aegisub/src/utils.h @@ -60,7 +60,6 @@ wxString DecodeRelativePath(wxString path,wxString reference); wxString AegiFloatToString(double value); wxString AegiIntegerToString(int value); wxString PrettySize(int bytes); -wxMenuItem *AppendBitmapMenuItem (wxMenu* parentMenu,int id,wxString text,wxString help,wxBitmap bmp,int pos=-1); int SmallestPowerOf2(int x); void GetWordBoundaries(const wxString text,IntPairVector &results,int start=0,int end=-1); bool IsWhitespace(wchar_t c);