diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 04ba49cad..02ddbf361 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -307,6 +307,7 @@ Text selbst. Unicode-Zeichen \, \t, \n, \r und ( ersetzen Escape-Sequenzen Exklusivmodus + Exklusivmodus aktivieren Auszuführende Programmdatei Verzeichnis in Konsole öffnen In Konsole ausführen diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index 36be4506e..2ee79ecb0 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -291,6 +291,7 @@ is hover a word that represents a color. Replace \, \t, \n, \r and (Unicode characters Escape Sequences Exclusive Mode + Enable Exclusive Mode Executable to Run Open directory in terminal Execute in terminal diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index b1abe9313..de1ce5a2d 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -290,6 +290,7 @@ texte lui-même. Remplacer \, \t, \n, \r et (caractères Unicode) Séquences d'échappement Mode exclusif + Activer le mode exclusif Exécutable à exécuter Ouvrir le répertoire dans le terminal Exécuter dans le terminal diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index d347ecc64..575b64cff 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -223,6 +223,7 @@ , 和 (Unicode字符 Exclusive Mode + 启用独占模式 在终端打开文件夹 在终端执行 Expand All diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 26f93222f..40bc5fc7e 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -1876,7 +1876,10 @@ void TerminalDisplay::onSizeChange() { mSession->resize( gridSize.getWidth(), gridSize.getHeight(), gridSize.getWidth() * cellSize.getWidth(), gridSize.getHeight() * cellSize.getHeight() ); - mDirtyLines.resize( gridSize.getHeight(), 1 ); + // Rendering must continue using the currently published snapshot until the worker + // publishes the resized grid. consumeSnapshot() updates mRows and mDirtyLines together; + // shrinking the dirty vector here leaves drawGrid() indexing it with the old mRows. + mDirtyLines.resize( std::max( mRows, gridSize.getHeight() ), true ); } if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() || diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index d9fd15896..8a79ddb2e 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -556,6 +556,8 @@ void App::maximizeTabWidget() { UIWindow::StyleConfig winCfg; winCfg.WinFlags = UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL | UI_WIN_NO_DECORATION; UIWindow* win = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, winCfg ); + mMaximizedTabWidgetWindow = win; + mMaximizedTabWidget = curTabWidget; win->setPixelsSize( getUISceneNode()->getPixelsSize() - PixelDensity::dpToPx( 64 ) ); win->setId( "detached_tab_widget_win" ); win->addClass( "tab_widget_cont" ); @@ -587,6 +589,7 @@ void App::maximizeTabWidget() { bool wasFirstSplit = tabWidgetParent->isType( UI_TYPE_SPLITTER ) && tabWidgetParent->asType()->getFirstWidget() == curTabWidget; auto nodeLink = UINodeLink::NewLink( curTabWidget ); + mMaximizedTabWidgetLink = nodeLink; if ( wasFirstSplit ) nodeLink->setClass( "was_first_split" ); curTabWidget->setParent( win ); @@ -626,27 +629,24 @@ void App::maximizeTabWidget() { void App::restoreMaximizedTabWidget() { if ( !App::instance() || !SceneManager::isActive() ) return; - auto sceneNode = appInstance->getUISceneNode(); - if ( !sceneNode ) - return; - auto nodeLink = sceneNode->getRoot()->find( "nodelink_tab_widget" ); - if ( !nodeLink ) - return; - auto splitterParent = nodeLink->getParent(); - nodeLink->setParent( sceneNode ); - auto curTabWidget = sceneNode->getRoot()->find( "detached_tab_widget" ); - if ( !curTabWidget ) + if ( !mMaximizedTabWidgetLink || !mMaximizedTabWidget ) return; + auto* nodeLink = mMaximizedTabWidgetLink; + auto* curTabWidget = mMaximizedTabWidget; + auto* win = mMaximizedTabWidgetWindow; + auto* splitterParent = nodeLink->getParent(); + nodeLink->setParent( mUISceneNode ); curTabWidget->setParent( splitterParent ); curTabWidget->setAnchors( 0 ); curTabWidget->setId( "" ); - if ( nodeLink->asType()->hasClass( "was_first_split" ) && - splitterParent->isType( UI_TYPE_SPLITTER ) ) { + if ( nodeLink->hasClass( "was_first_split" ) && splitterParent->isType( UI_TYPE_SPLITTER ) ) { splitterParent->asType()->swap(); } nodeLink->close(); - auto win = mUISceneNode->find( "detached_tab_widget_win" ); - if ( win ) + mMaximizedTabWidgetLink = nullptr; + mMaximizedTabWidget = nullptr; + mMaximizedTabWidgetWindow = nullptr; + if ( win && !win->isClosing() ) win->close(); } @@ -1734,6 +1734,16 @@ void App::onTabCreated( UITab* tab, UIWidget* ) { if ( tab->getOwnedWidget()->isType( UI_TYPE_CODEEDITOR ) || tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) { + if ( tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) { + menu->addSeparator(); + auto* terminal = tab->getOwnedWidget()->asType(); + menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ), + terminal->getExclusiveMode(), + getKeybind( UITerminal::getExclusiveModeToggleCommandName() ) ) + ->setId( UITerminal::getExclusiveModeToggleCommandName() ); + menuAdd( "rename_session", "Rename Session", "", "terminal-rename" ); + } + menu->addSeparator(); bool enabled = tab->getTabWidget()->getTabCount() > 1; @@ -5073,6 +5083,11 @@ void App::init( InitParameters& params ) { std::function onMsgBoxCloseCb ) -> bool { if ( widget == nullptr || widget->getData() == 0 ) return true; + if ( auto* detachedTabWidget = + mUISceneNode->getRoot()->find( "detached_tab_widget" ); + detachedTabWidget && detachedTabWidget->inParentTreeOf( widget ) ) { + restoreMaximizedTabWidget(); + } if ( widget->isType( UI_TYPE_CODEEDITOR ) ) { return mSplitter->tryCodeEditorClose( widget->asType(), focusTabBehavior, onMsgBoxCloseCb ); diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index a1e563675..9bc45dd2b 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -680,6 +680,9 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UIMainLayout* mMainLayout{ nullptr }; UILayout* mBaseLayout{ nullptr }; UILayout* mImageLayout{ nullptr }; + UIWindow* mMaximizedTabWidgetWindow{ nullptr }; + UITabWidget* mMaximizedTabWidget{ nullptr }; + UINodeLink* mMaximizedTabWidgetLink{ nullptr }; UITextView* mDocInfo{ nullptr }; std::string mDocInfoLineAbbr; std::string mDocInfoColAbbr; diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 014c4c1dc..3bd8b7e5f 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -40,39 +41,76 @@ struct TerminalLaunchConfig { bool closeOnExit{ false }; }; -EE::Window::Window* appWindow{ nullptr }; -UISceneNode* scene{ nullptr }; -UILinearLayout* mainLayout{ nullptr }; -UITabWidgetSplitter* tabSplitter{ nullptr }; -FontTrueType* terminalFont{ nullptr }; -UIIcon* terminalIcon{ nullptr }; -UIMessageBox* closeDialog{ nullptr }; -UIWidget* closeDialogWidget{ nullptr }; -TerminalLaunchConfig terminalConfig; -std::map terminalColorSchemes; -const TerminalColorScheme* selectedColorScheme{ nullptr }; -Float terminalFontSize{ 12 }; -bool warnBeforeClose{ false }; -bool closeApproved{ false }; -bool benchmarkMode{ false }; -Clock secondsCounter; -SmallVector pendingExitCloseTabs; - -void updateWindowTitle(); - -class TerminalSplitterClient : public UITabWidgetSplitter::Client { +class EtermApp { public: - void onTabCreated( UITab* tab, UIWidget* ) override { - if ( terminalIcon ) - tab->setIcon( terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); - } + int run( int argc, char* argv[] ); - void onWidgetFocusChange( UIWidget* ) override { updateWindowTitle(); } + class TerminalSplitterClient : public UITabWidgetSplitter::Client { + public: + explicit TerminalSplitterClient( EtermApp& app ) : mApp( app ) {} + + void onTabCreated( UITab* tab, UIWidget* ) override; + void onWidgetFocusChange( UIWidget* ) override; + + private: + EtermApp& mApp; + }; + + private: + std::string getResourcePath() const; + String i18n( const std::string& key, const String& defaultValue ) const; + void loadColorSchemes( const std::string& resPath ); + static UITerminal* terminalFromTab( UITab* tab ); + void updateWindowTitle(); + bool hasTerminals() const; + static bool hasRunningChildren( UITab* tab ); + void closeTab( UITab* tab ); + void queueExitCloseTab( UITab* tab ); + void queueExitedTabs(); + void requestCloseTab( UITab* tab ); + void renameSession( UITerminal* terminal ); + void maximizeTabWidget( UITabWidget* tabWidget ); + void restoreMaximizedTabWidget(); + void configureTab( UITab* tab ); + UITerminal* createTerminal( UITabWidget* target = nullptr ); + UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal ); + void addTabKeyBindings( UITerminal* terminal ); + bool closeWindow( EE::Window::Window* ); + + EE::Window::Window* appWindow{ nullptr }; + UISceneNode* scene{ nullptr }; + UILinearLayout* mainLayout{ nullptr }; + UITabWidgetSplitter* tabSplitter{ nullptr }; + FontTrueType* terminalFont{ nullptr }; + UIIcon* terminalIcon{ nullptr }; + UIMessageBox* closeDialog{ nullptr }; + UIWidget* closeDialogWidget{ nullptr }; + UIWindow* maximizedTabWidgetWindow{ nullptr }; + UITabWidget* maximizedTabWidget{ nullptr }; + UINodeLink* maximizedTabWidgetLink{ nullptr }; + TerminalLaunchConfig terminalConfig; + std::map terminalColorSchemes; + const TerminalColorScheme* selectedColorScheme{ nullptr }; + Float terminalFontSize{ 12 }; + bool warnBeforeClose{ false }; + bool closeApproved{ false }; + bool benchmarkMode{ false }; + Clock secondsCounter; + SmallVector pendingExitCloseTabs; + TerminalSplitterClient splitterClient{ *this }; }; -TerminalSplitterClient splitterClient; +void EtermApp::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* ) { + if ( mApp.terminalIcon ) + tab->setIcon( mApp.terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); + mApp.configureTab( tab ); +} -std::string getResourcePath() { +void EtermApp::TerminalSplitterClient::onWidgetFocusChange( UIWidget* ) { + mApp.updateWindowTitle(); +} + +std::string EtermApp::getResourcePath() const { std::string resPath = Sys::getProcessPath(); #if EE_PLATFORM == EE_PLATFORM_MACOS if ( String::contains( resPath, "ecode.app" ) ) { @@ -92,7 +130,11 @@ std::string getResourcePath() { return resPath; } -void loadColorSchemes( const std::string& resPath ) { +String EtermApp::i18n( const std::string& key, const String& defaultValue ) const { + return scene ? scene->i18n( key, defaultValue ) : defaultValue; +} + +void EtermApp::loadColorSchemes( const std::string& resPath ) { auto colorSchemes = TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" ); const std::string configColorSchemesPath = @@ -111,16 +153,16 @@ void loadColorSchemes( const std::string& resPath ) { } } -UITerminal* terminalFromTab( UITab* tab ) { +UITerminal* EtermApp::terminalFromTab( UITab* tab ) { return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ? tab->getOwnedWidget()->asType() : nullptr; } -void updateWindowTitle() { +void EtermApp::updateWindowTitle() { if ( !appWindow ) return; - std::string title{ "eterm" }; + String title{ "eterm" }; if ( tabSplitter ) { if ( auto* terminal = tabSplitter->getCurWidget() && tabSplitter->getCurWidget()->isType( UI_TYPE_TERMINAL ) @@ -134,12 +176,12 @@ void updateWindowTitle() { if ( benchmarkMode ) { title += " - "; title += String::toString( appWindow->getFPS() ); - title += " FPS"; + title += " " + i18n( "fps", "FPS" ); } appWindow->setTitle( title ); } -bool hasTerminals() { +bool EtermApp::hasTerminals() const { bool found = false; if ( tabSplitter ) tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) { @@ -149,30 +191,30 @@ bool hasTerminals() { return found; } -bool hasRunningChildren( UITab* tab ) { +bool EtermApp::hasRunningChildren( UITab* tab ) { auto* terminal = terminalFromTab( tab ); return terminal && terminal->getTerm() && Sys::processHasChildren( terminal->getTerm()->getProcessId() ); } -void closeTab( UITab* tab ) { +void EtermApp::closeTab( UITab* tab ) { if ( !tabSplitter || !tab || !tab->getOwnedWidget() ) return; tabSplitter->closeTab( tab->getOwnedWidget()->asType(), UITabWidget::FocusTabBehavior::Default ); } -void queueExitCloseTab( UITab* tab ) { +void EtermApp::queueExitCloseTab( UITab* tab ) { if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) == pendingExitCloseTabs.end() ) { pendingExitCloseTabs.emplace_back( tab ); } } -void queueExitedTabs() { +void EtermApp::queueExitedTabs() { if ( !terminalConfig.closeOnExit ) return; - tabSplitter->forEachTab( []( UITab* tab ) { + tabSplitter->forEachTab( [this]( UITab* tab ) { auto* terminal = terminalFromTab( tab ); if ( !terminal || !terminal->getTerm() ) return; @@ -183,7 +225,7 @@ void queueExitedTabs() { } ); } -void requestCloseTab( UITab* tab ) { +void EtermApp::requestCloseTab( UITab* tab ) { if ( !warnBeforeClose || !hasRunningChildren( tab ) ) { closeTab( tab ); return; @@ -192,14 +234,15 @@ void requestCloseTab( UITab* tab ) { return; closeDialog = UIMessageBox::New( UIMessageBox::OK_CANCEL, - "Are you sure you want to close this terminal?\nIt is still running a process." ); + i18n( "close_terminal_running_process_confirm", + "Are you sure you want to close this terminal?\nIt is still running a process." ) ); closeDialogWidget = tab->getOwnedWidget()->asType(); closeDialog->setTitle( "eterm" ); - closeDialog->on( Event::OnConfirm, []( const Event* ) { + closeDialog->on( Event::OnConfirm, [this]( const Event* ) { if ( closeDialogWidget && tabSplitter->ownedWidgetExists( closeDialogWidget ) ) tabSplitter->closeTab( closeDialogWidget, UITabWidget::FocusTabBehavior::Default ); } ); - closeDialog->on( Event::OnClose, []( const Event* ) { + closeDialog->on( Event::OnClose, [this]( const Event* ) { closeDialog = nullptr; closeDialogWidget = nullptr; } ); @@ -207,9 +250,194 @@ void requestCloseTab( UITab* tab ) { closeDialog->showWhenReady(); } -void addTabKeyBindings( UITerminal* terminal, UITab* tab ); +void EtermApp::renameSession( UITerminal* terminal ) { + if ( !terminal ) + return; + auto* msgBox = + UIMessageBox::New( UIMessageBox::INPUT, i18n( "new_terminal_name", "New terminal name:" ) ); + msgBox->setTitle( "eterm" ); + msgBox->getTextInput()->setHint( i18n( "any_name_ellipsis", "Any name..." ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); + msgBox->on( Event::OnConfirm, [msgBox, terminal]( const Event* ) { + terminal->setTitle( msgBox->getTextInput()->getText().toUtf8() ); + msgBox->close(); + terminal->setFocus(); + } ); + msgBox->showWhenReady(); +} -UITerminal* createTerminal( UITabWidget* target = nullptr ) { +void EtermApp::maximizeTabWidget( UITabWidget* tabWidget ) { + if ( !tabWidget || scene->getRoot()->hasChild( "detached_tab_widget_win" ) ) + return; + + UIWindow::StyleConfig winCfg; + winCfg.WinFlags = UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL | UI_WIN_NO_DECORATION; + auto* win = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, winCfg ); + maximizedTabWidgetWindow = win; + maximizedTabWidget = tabWidget; + win->setPixelsSize( scene->getPixelsSize() - PixelDensity::dpToPx( 64 ) ); + win->setId( "detached_tab_widget_win" ); + win->getModalWidget()->onClick( [this]( auto ) { restoreMaximizedTabWidget(); } ); + win->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_LEFT | UI_ANCHOR_BOTTOM | UI_ANCHOR_RIGHT ); + win->toFront(); + win->center(); + win->setKeyBindingCommand( "close-maximized-tab-widget", + [this] { restoreMaximizedTabWidget(); } ); + win->getKeyBindings().addKeybind( { KEY_ESCAPE }, "close-maximized-tab-widget" ); + win->setCheckEphemeralCloseFn( [this]( Node* focusNode ) { + if ( focusNode->isType( UI_TYPE_POPUPMENU ) ) + return false; + if ( focusNode->getSceneNode()->isUISceneNode() ) { + auto* sceneNode = static_cast( focusNode->getSceneNode() ); + auto* widgetTreeView = sceneNode->getRoot()->hasChild( "widget-tree-view" ); + if ( widgetTreeView && + ( focusNode == widgetTreeView || widgetTreeView->inParentTreeOf( focusNode ) ) ) + return false; + } + return true; + } ); + auto* tabWidgetParent = tabWidget->getParent(); + const bool wasFirstSplit = tabWidgetParent->isType( UI_TYPE_SPLITTER ) && + tabWidgetParent->asType()->getFirstWidget() == tabWidget; + auto* nodeLink = UINodeLink::NewLink( tabWidget ); + maximizedTabWidgetLink = nodeLink; + if ( wasFirstSplit ) + nodeLink->setClass( "was_first_split" ); + tabWidget->setParent( win ); + tabWidget->setId( "detached_tab_widget" ); + tabWidget->setPixelsPosition( Vector2f::Zero ); + tabWidget->setPixelsSize( win->getContainer()->getPixelsSize() ); + tabWidget->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_LEFT | UI_ANCHOR_BOTTOM | UI_ANCHOR_RIGHT ); + if ( !tabWidget->inParentTreeOf( scene->getEventDispatcher()->getFocusNode() ) ) + tabWidget->getTabSelected()->getOwnedWidget()->setFocus(); + win->on( Event::OnWindowClose, [this]( auto ) { restoreMaximizedTabWidget(); } ); + nodeLink->setParent( tabWidgetParent ); + nodeLink->setId( "nodelink_tab_widget" ); + if ( wasFirstSplit && tabWidgetParent->isType( UI_TYPE_SPLITTER ) ) + tabWidgetParent->asType()->swap(); + + auto* fullscreenImg = UIImage::New(); + fullscreenImg->unsetFlags( UI_AUTO_SIZE ); + fullscreenImg->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + fullscreenImg->setParent( win ); + fullscreenImg->setSize( { 24, tabWidget->getTabBar()->getSize().getHeight() } ); + fullscreenImg->setPosition( { tabWidget->getSize().getWidth() - 24, 0 } ); + fullscreenImg->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_RIGHT ); + if ( auto* icon = scene->findIcon( "fullscreen" ) ) + fullscreenImg->setDrawable( icon->createDrawable( PixelDensity::dpToPxI( 16 ) ) ); + fullscreenImg->setVerticalAlign( UI_VALIGN_CENTER ); + fullscreenImg->setHorizontalAlign( UI_HALIGN_CENTER ); + fullscreenImg->addClass( "pseudo_anchor" ); + fullscreenImg->toFront(); + fullscreenImg->onClick( [this]( const Event* ) { + restoreMaximizedTabWidget(); + appWindow->getCursorManager()->set( Cursor::SysType::SysArrow ); + } ); +} + +void EtermApp::restoreMaximizedTabWidget() { + if ( !scene || !SceneManager::isActive() ) + return; + if ( !maximizedTabWidgetLink || !maximizedTabWidget ) + return; + auto* nodeLink = maximizedTabWidgetLink; + auto* curTabWidget = maximizedTabWidget; + auto* win = maximizedTabWidgetWindow; + auto* splitterParent = nodeLink->getParent(); + nodeLink->setParent( scene ); + curTabWidget->setParent( splitterParent ); + curTabWidget->setAnchors( 0 ); + curTabWidget->setId( "" ); + if ( nodeLink->hasClass( "was_first_split" ) && splitterParent->isType( UI_TYPE_SPLITTER ) ) { + splitterParent->asType()->swap(); + } + nodeLink->close(); + maximizedTabWidgetLink = nullptr; + maximizedTabWidget = nullptr; + maximizedTabWidgetWindow = nullptr; + if ( win && !win->isClosing() ) + win->close(); +} + +void EtermApp::configureTab( UITab* tab ) { + tab->on( Event::OnCreateContextMenu, [this]( const Event* event ) { + const auto* menuEvent = static_cast( event ); + auto* menu = menuEvent->getMenu(); + auto* clickedTab = event->getNode()->asType(); + auto* terminal = terminalFromTab( clickedTab ); + if ( !menu || !terminal || !clickedTab->getTabWidget() ) + return; + + const auto addItem = [this, menu]( const String& text, const std::string& icon, + const std::string& command ) { + DrawablePtr drawable; + if ( auto* menuIcon = scene->findIcon( icon ) ) + drawable = menuIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ); + auto* item = menu->add( text, std::move( drawable ) ); + item->setId( command ); + return item; + }; + addItem( i18n( "close_tab", "Close Tab" ), "document-close", "close-tab" ); + addItem( i18n( "close_other_tabs", "Close Other Tabs" ), "", "close-other-tabs" ); + addItem( i18n( "close_clean_tabs", "Close Clean Tabs" ), "", "close-clean-tabs" ); + addItem( i18n( "close_all_tabs", "Close All Tabs" ), "", "close-all-tabs" ); + addItem( i18n( "close_tabs_to_the_left", "Close Tabs To The Left" ), "", + "close-tabs-to-the-left" ); + addItem( i18n( "close_tabs_to_the_right", "Close Tabs To The Right" ), "", + "close-tabs-to-the-right" ); + + menu->addSeparator(); + auto* exclusiveMode = + menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ), + terminal->getExclusiveMode() ); + exclusiveMode->setId( UITerminal::getExclusiveModeToggleCommandName() ); + addItem( i18n( "rename_session", "Rename Session" ), "", "terminal-rename" ); + + menu->addSeparator(); + const bool canMove = clickedTab->getTabWidget()->getTabCount() > 1; + addItem( i18n( "move_tab_to_start", "Move Tab To Start" ), "window", "move-tab-to-start" ) + ->setEnabled( canMove ); + addItem( i18n( "move_tab_to_end", "Move Tab To End" ), "window", "move-tab-to-end" ) + ->setEnabled( canMove ); + + menu->addSeparator(); + addItem( scene->getRoot()->hasChild( "detached_tab_widget_win" ) + ? i18n( "restore_maximized_tab_widget", "Restore Maximized Tab Widget" ) + : i18n( "maximize_tab_widget", "Maximize Tab Widget" ), + "fullscreen", + scene->getRoot()->hasChild( "detached_tab_widget_win" ) + ? "restore-maximized-tab-widget" + : "maximize-tab-widget" ); + + menu->on( Event::OnItemClicked, [this, clickedTab, terminal]( const Event* itemEvent ) { + if ( !itemEvent->getNode()->isType( UI_TYPE_MENUITEM ) ) + return; + const auto& command = itemEvent->getNode()->getId(); + auto* previous = tabSplitter->getCurWidget(); + tabSplitter->setCurrentWidget( terminal ); + + if ( command == "close-clean-tabs" ) { + tabSplitter->tryCloseAllTabs( terminal, UITabWidget::FocusTabBehavior::Default ); + } else if ( command == "move-tab-to-start" ) { + clickedTab->getTabWidget()->moveTab( clickedTab, 0 ); + } else if ( command == "move-tab-to-end" ) { + clickedTab->getTabWidget()->moveTab( clickedTab, + clickedTab->getTabWidget()->getTabCount() ); + } else if ( command == "maximize-tab-widget" ) { + maximizeTabWidget( clickedTab->getTabWidget() ); + } else if ( command == "restore-maximized-tab-widget" ) { + restoreMaximizedTabWidget(); + } else { + terminal->execute( command ); + } + + if ( previous && tabSplitter->ownedWidgetExists( previous ) ) + tabSplitter->setCurrentWidget( previous ); + } ); + } ); +} + +UITerminal* EtermApp::createTerminal( UITabWidget* target ) { if ( !target && tabSplitter ) { auto* current = tabSplitter->getCurWidget(); target = current ? tabSplitter->tabWidgetFromWidget( current ) @@ -239,14 +467,17 @@ UITerminal* createTerminal( UITabWidget* target = nullptr ) { if ( selectedColorScheme ) terminal->setColorScheme( *selectedColorScheme ); - auto* tab = tabSplitter->createWidgetInTabWidget( target, terminal, "Terminal" ).first; - addTabKeyBindings( terminal, tab ); - terminal->on( Event::OnTitleChange, [tab, terminal]( const Event* ) { - tab->setText( terminal->getTitle().empty() ? "Terminal" : terminal->getTitle() ); + auto* tab = + tabSplitter->createWidgetInTabWidget( target, terminal, i18n( "terminal", "Terminal" ) ) + .first; + addTabKeyBindings( terminal ); + terminal->on( Event::OnTitleChange, [this, tab, terminal]( const Event* ) { + tab->setText( terminal->getTitle().empty() ? i18n( "terminal", "Terminal" ) + : String( terminal->getTitle() ) ); if ( tabSplitter->getCurWidget() == terminal ) updateWindowTitle(); } ); - terminal->getTerm()->pushEventCallback( [tab]( const TerminalDisplay::Event& event ) { + terminal->getTerm()->pushEventCallback( [this, tab]( const TerminalDisplay::Event& event ) { if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT ) queueExitCloseTab( tab ); } ); @@ -258,38 +489,48 @@ UITerminal* createTerminal( UITabWidget* target = nullptr ) { return terminal; } -UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal ) { +UITerminal* EtermApp::createTerminalSplit( SplitDirection direction, UITerminal* terminal ) { auto* source = terminal ? tabSplitter->tabWidgetFromWidget( terminal ) : nullptr; auto* target = source ? tabSplitter->splitTabWidget( direction, source ) : nullptr; return target ? createTerminal( target ) : nullptr; } -void addTabKeyBindings( UITerminal* terminal, UITab* tab ) { - terminal->setCommand( "create-new-terminal", [] { createTerminal(); } ); - terminal->setCommand( "close-tab", [tab] { requestCloseTab( tab ); } ); - terminal->setCommand( "next-tab", [terminal] { +void EtermApp::addTabKeyBindings( UITerminal* terminal ) { + tabSplitter->registerSplitterCommands( *terminal ); + terminal->setCommand( "create-new-terminal", [this] { createTerminal(); } ); + terminal->setCommand( "debug-widget-tree-view", + [this] { UIWidgetInspector::create( scene ); } ); + terminal->setCommand( "terminal-rename", [this, terminal] { renameSession( terminal ); } ); + terminal->setCommand( UITerminal::getExclusiveModeToggleCommandName(), [terminal] { + terminal->setExclusiveMode( !terminal->getExclusiveMode() ); + } ); + terminal->setCommand( "next-tab", [this, terminal] { if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) tabs->focusNextTab(); } ); - terminal->setCommand( "previous-tab", [terminal] { + terminal->setCommand( "previous-tab", [this, terminal] { if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) tabs->focusPreviousTab(); } ); - terminal->setCommand( "split-right", - [terminal] { createTerminalSplit( SplitDirection::Right, terminal ); } ); - terminal->setCommand( "split-bottom", - [terminal] { createTerminalSplit( SplitDirection::Bottom, terminal ); } ); - terminal->setCommand( "split-left", - [terminal] { createTerminalSplit( SplitDirection::Left, terminal ); } ); - terminal->setCommand( "split-top", - [terminal] { createTerminalSplit( SplitDirection::Top, terminal ); } ); + terminal->setCommand( "split-right", [this, terminal] { + createTerminalSplit( SplitDirection::Right, terminal ); + } ); + terminal->setCommand( "split-bottom", [this, terminal] { + createTerminalSplit( SplitDirection::Bottom, terminal ); + } ); + terminal->setCommand( + "split-left", [this, terminal] { createTerminalSplit( SplitDirection::Left, terminal ); } ); + terminal->setCommand( + "split-top", [this, terminal] { createTerminalSplit( SplitDirection::Top, terminal ); } ); terminal->setCommand( "switch-to-previous-split", - [terminal] { tabSplitter->switchPreviousSplit( terminal ); } ); + [this, terminal] { tabSplitter->switchPreviousSplit( terminal ); } ); terminal->setCommand( "switch-to-next-split", - [terminal] { tabSplitter->switchNextSplit( terminal ); } ); + [this, terminal] { tabSplitter->switchNextSplit( terminal ); } ); terminal->addKeyBinding( { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" ); terminal->addKeyBinding( { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" ); + terminal->addKeyBinding( { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, + "debug-widget-tree-view" ); terminal->addKeyBinding( { KEY_PAGEDOWN, KEYMOD_CTRL }, "next-tab" ); terminal->addKeyBinding( { KEY_PAGEUP, KEYMOD_CTRL }, "previous-tab" ); terminal->addKeyBinding( { KEY_TAB, KEYMOD_CTRL }, "next-tab" ); @@ -310,7 +551,7 @@ void addTabKeyBindings( UITerminal* terminal, UITab* tab ) { "switch-to-next-split" ); } -bool closeWindow( EE::Window::Window* ) { +bool EtermApp::closeWindow( EE::Window::Window* ) { if ( closeApproved || !warnBeforeClose ) return true; bool running = false; @@ -321,13 +562,14 @@ bool closeWindow( EE::Window::Window* ) { return false; closeDialog = UIMessageBox::New( UIMessageBox::OK_CANCEL, - "Are you sure you want to close this window? It is still running a process." ); + i18n( "close_window_running_process_confirm", + "Are you sure you want to close this window? It is still running a process." ) ); closeDialog->setTitle( "eterm" ); - closeDialog->on( Event::OnConfirm, []( const Event* ) { + closeDialog->on( Event::OnConfirm, [this]( const Event* ) { closeApproved = true; appWindow->close(); } ); - closeDialog->on( Event::OnClose, []( const Event* ) { + closeDialog->on( Event::OnClose, [this]( const Event* ) { closeDialog = nullptr; closeDialogWidget = nullptr; } ); @@ -336,11 +578,9 @@ bool closeWindow( EE::Window::Window* ) { return false; } -} // namespace - -EE_MAIN_FUNC int main( int argc, char* argv[] ) { +int EtermApp::run( int argc, char* argv[] ) { #ifdef EE_DEBUG - Log::instance()->setLogToStdOut( true ); + Log::instance()->setLogToStdOut( !Runtime::isOffscreen() ); Log::instance()->setLiveWrite( true ); #endif args::ArgumentParser parser( "eterm" ); @@ -407,6 +647,9 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { parser, "warn-before-closing", "Prompts for confirmation if a program is still running when closing the terminal.", { "warn-before-closing" } ); + args::Flag alwaysShowTabBar( parser, "always-show-tab-bar", + "Always show the tab bar, even with a single tab.", + { "always-show-tab-bar" } ); args::ValueFlag initialTabs( parser, "tabs", "Number of initial terminal tabs", { "tabs" }, 1 ); @@ -476,6 +719,23 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { return EXIT_FAILURE; FileSystem::changeWorkingDirectory( initialWorkingDirectory ); appWindow->setClearColor( RGB( 0, 0, 0 ) ); + scene->getUIThemeManager()->setDefaultEffectsEnabled( false ); + scene->combineStyleSheet( R"css( + TabWidget { + max-tab-width: 200dp; + } + Tab > Tab::Text { + text-overflow: ellipsis; + } + .pseudo_anchor { + tint: var(--floating-icon); + cursor: arrow; + } + .pseudo_anchor:hover { + tint: var(--primary); + cursor: hand; + } + )css" ); auto& resourceScope = *scene->getResourceScope(); auto remixIconFont = FontTrueType::New( "eterm-remixicon", resourceScope ); @@ -539,21 +799,29 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { mainLayout->setPixelsSize( appWindow->getSize().asFloat() ); tabSplitter = UITabWidgetSplitter::New( &splitterClient, scene ); - tabSplitter->setHideTabBarOnSingleTab( true ); - tabSplitter->setTabTryCloseCallback( - []( UIWidget* widget, UITabWidget::FocusTabBehavior, std::function ) { - if ( warnBeforeClose ) { - auto* tab = tabSplitter->getTabFromWidget( widget ); - if ( hasRunningChildren( tab ) ) { - requestCloseTab( tab ); - return false; - } + tabSplitter->setHideTabBarOnSingleTab( !alwaysShowTabBar.Get() ); + tabSplitter->setCanCreateSplitFn( [this]( SplitDirection, UIWidget* ) { + restoreMaximizedTabWidget(); + return true; + } ); + tabSplitter->setTabTryCloseCallback( [this]( UIWidget* widget, UITabWidget::FocusTabBehavior, + std::function ) { + if ( auto* detachedTabWidget = scene->getRoot()->find( "detached_tab_widget" ); + widget && detachedTabWidget && detachedTabWidget->inParentTreeOf( widget ) ) { + restoreMaximizedTabWidget(); + } + if ( warnBeforeClose ) { + auto* tab = tabSplitter->getTabFromWidget( widget ); + if ( hasRunningChildren( tab ) ) { + requestCloseTab( tab ); + return false; } - return true; - } ); - tabSplitter->setOnTabWidgetCreateCb( []( UITabWidget* tabs ) { - tabs->on( Event::OnTabSelected, []( const Event* ) { updateWindowTitle(); } ); - tabs->on( Event::OnTabClosed, []( const Event* event ) { + } + return true; + } ); + tabSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabs ) { + tabs->on( Event::OnTabSelected, [this]( const Event* ) { updateWindowTitle(); } ); + tabs->on( Event::OnTabClosed, [this]( const Event* event ) { auto* closedTab = static_cast( event )->getTab(); pendingExitCloseTabs.erase( std::remove( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), closedTab ), @@ -575,15 +843,17 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { for ( size_t tab = 0; tab < eemax( static_cast( 1 ), initialTabs.Get() ); ++tab ) { if ( !createTerminal( tabs ) ) { - appWindow->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm", - "Operating System not supported." ); + appWindow->showMessageBox( + EE::Window::Window::MessageBoxType::Error, "eterm", + i18n( "operating_system_not_supported", "Operating System not supported." ) ); return EXIT_FAILURE; } } - appWindow->setCloseRequestCallback( &closeWindow ); + appWindow->setCloseRequestCallback( + [this]( EE::Window::Window* window ) { return closeWindow( window ); } ); app.setShowMemoryManagerResult( true ); - appWindow->runMainLoop( [] { + appWindow->runMainLoop( [this] { appWindow->getInput()->update(); SceneManager::instance()->update(); queueExitedTabs(); @@ -610,3 +880,10 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { } ); return EXIT_SUCCESS; } + +} // namespace + +EE_MAIN_FUNC int main( int argc, char* argv[] ) { + EtermApp app; + return app.run( argc, argv ); +}