mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-25 15:53:11 +03:00
fix(eefm, window): align pane focus and support hidden launches
Derive the active pane from keyboard focus, retaining the last focused pane only while other UI owns focus. Keep inactive scrolling and background middle-click tabs from changing the command target. Use Breeze's default scrollbar thumb color in focused panes while preserving muted inactive thumbs and theme hover colors. Add EEPP_WINDOW_HIDDEN=1 to force native windows hidden, including secondary windows and explicit presentation requests. Document the flag and add regression coverage.
This commit is contained in:
@@ -134,6 +134,12 @@ native runtime on a usable desktop. Use the native/Xvfb paths below for examples
|
||||
required check cannot run with the headless runtime. Verify the renderer before using an offscreen
|
||||
run to assess hardware rendering or performance.
|
||||
|
||||
When the headless driver is unavailable, use `EEPP_RUNTIME=native EEPP_WINDOW_HIDDEN=1` for ordinary
|
||||
application/example launches and inspector automation. This preserves native rendering while
|
||||
forcing primary and secondary eepp windows hidden, including explicit `show()`/restore requests;
|
||||
it avoids temporary source edits to add `WindowStyle::Hidden`. A usable native display is still
|
||||
required. See `docs/articles/ui_inspector.md` for the environment flag's behavior.
|
||||
|
||||
Xvfb does not support the multisampled OpenGL contexts requested by some eepp examples. In
|
||||
particular, `src/examples/ui_html/ui_html.cpp` normally requests 4x MSAA. Launching that binary
|
||||
through `xvfb-run` or `projects/scripts/xvfb-run-eepp` can therefore fail immediately with
|
||||
|
||||
@@ -40,8 +40,11 @@ database, launching applications) may use local platform APIs. See `docs/article
|
||||
|
||||
AI agents should prefer `EEPP_RUNTIME=headless` when launching ordinary applications or examples
|
||||
and when automating their UI with the inspector. This avoids opening desktop windows or interrupting
|
||||
the human's keyboard input and focus. Coordinate interactive checks that require visible native
|
||||
windows with the user before driving them.
|
||||
the human's keyboard input and focus. When headless rendering is unavailable (notably on macOS),
|
||||
or native rendering needs verification, use `EEPP_RUNTIME=native EEPP_WINDOW_HIDDEN=1` instead of
|
||||
modifying application sources to add `WindowStyle::Hidden`. This also keeps secondary native windows
|
||||
hidden, even if the application calls `show()` or restores them. Coordinate interactive checks that
|
||||
require visible native windows with the user before driving them.
|
||||
|
||||
Unit tests and benchmarks are different: run their release binaries with the native runtime on a
|
||||
usable desktop. The unit-test build defines `EE_UNIT_TESTS`, which adds `WindowStyle::Hidden` to
|
||||
|
||||
@@ -39,6 +39,26 @@ Copy the `port` and generated `token` from that startup line into the client env
|
||||
The default port is selected by the OS at launch, so the client cannot infer it in advance.
|
||||
If you set `EEPP_INSPECTOR_TOKEN` yourself, use that value for the client instead.
|
||||
|
||||
## Hidden native windows
|
||||
|
||||
Set `EEPP_WINDOW_HIDDEN=1` to inspect an application without opening desktop windows:
|
||||
|
||||
```sh
|
||||
EEPP_RUNTIME=native EEPP_WINDOW_HIDDEN=1 EEPP_INSPECTOR=1 ./bin/eefm
|
||||
```
|
||||
|
||||
The flag forces every eepp window created through `Engine::createWindow()` hidden, including
|
||||
secondary application windows and native dialog hosts. Native `show()`, `raise()`, `restore()`,
|
||||
`maximize()`, and fullscreen requests cannot reveal these windows while the flag is enabled.
|
||||
Widget visibility, rendering, screenshots, and inspector input remain available. Set the flag before
|
||||
launch; only the exact value `1` enables it. Unset, empty, `0`, and all other values disable it.
|
||||
|
||||
This flag preserves the selected runtime and renderer. With `EEPP_RUNTIME=native`, it still needs a
|
||||
usable native display, but keeps the windows hidden. Use this when the headless SDL driver is
|
||||
unavailable (for example, on macOS) or when checking native rendering without interrupting desktop
|
||||
focus. `EEPP_RUNTIME=headless` instead selects an offscreen video driver. The visibility flag does
|
||||
not suppress operating-system dialogs opened outside eepp's window API.
|
||||
|
||||
## Python client
|
||||
|
||||
`projects/scripts/eepp-inspect.py` requires Python 3 and only the standard library. It reads `EEPP_INSPECTOR_HOST`, `EEPP_INSPECTOR_PORT`, and `EEPP_INSPECTOR_TOKEN`, or the corresponding `--host`, `--port`, and `--token` options. Use `--pretty` for indented JSON. Default stdout is compact JSON only; diagnostics go to stderr. Ordinary commands connect, authenticate, print one response, and disconnect.
|
||||
|
||||
@@ -12,7 +12,7 @@ enum class RuntimeMode : Uint8 {
|
||||
Terminal //!< Offscreen rendering presented through a terminal transport.
|
||||
};
|
||||
|
||||
/** Process-level runtime selection, resolved once from EEPP_RUNTIME. */
|
||||
/** Process-level runtime selection and window visibility overrides. */
|
||||
class EE_API Runtime {
|
||||
public:
|
||||
/** @return The process runtime mode resolved from EEPP_RUNTIME on first use. */
|
||||
@@ -21,6 +21,10 @@ class EE_API Runtime {
|
||||
/** @return True when the runtime requires an offscreen video driver and logical framebuffer. */
|
||||
static bool isOffscreen();
|
||||
|
||||
/** @return True when EEPP_WINDOW_HIDDEN is exactly "1". This does not change the runtime
|
||||
* or renderer. Set it before creating windows to suppress native window presentation. */
|
||||
static bool forceHiddenWindows();
|
||||
|
||||
/** @return A stable lowercase name for the resolved runtime mode. */
|
||||
static const char* modeName();
|
||||
};
|
||||
|
||||
@@ -524,6 +524,8 @@ void WindowSDL::setGLConfig() {
|
||||
}
|
||||
|
||||
void WindowSDL::toggleFullscreen() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
Log::info( "toggleFullscreen called: %s", isWindowed() ? "is windowed" : "is fullscreen" );
|
||||
|
||||
if ( isWindowed() ) {
|
||||
@@ -615,6 +617,8 @@ void WindowSDL::onWindowResize( Uint32 width, Uint32 height ) {
|
||||
}
|
||||
|
||||
void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
windowed = true;
|
||||
if ( ( !width || !height ) ) {
|
||||
width = mWindow.DesktopResolution.getWidth();
|
||||
height = mWindow.DesktopResolution.getHeight();
|
||||
@@ -817,7 +821,7 @@ void WindowSDL::minimize() {
|
||||
}
|
||||
|
||||
void WindowSDL::maximize() {
|
||||
if ( Runtime::isOffscreen() )
|
||||
if ( Runtime::isOffscreen() || Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_MaximizeWindow( mSDLWindow );
|
||||
}
|
||||
@@ -835,10 +839,14 @@ void WindowSDL::hide() {
|
||||
}
|
||||
|
||||
void WindowSDL::raise() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_RaiseWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
void WindowSDL::restore() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_RestoreWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
@@ -856,6 +864,8 @@ void WindowSDL::flash( WindowFlashOperation op ) {
|
||||
}
|
||||
|
||||
void WindowSDL::show() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_ShowWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
|
||||
@@ -486,6 +486,8 @@ void WindowSDL::setGLConfig() {
|
||||
}
|
||||
|
||||
void WindowSDL::toggleFullscreen() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
Log::info( "toggleFullscreen called: %s", isWindowed() ? "is windowed" : "is fullscreen" );
|
||||
|
||||
if ( isWindowed() ) {
|
||||
@@ -579,6 +581,8 @@ void WindowSDL::onWindowResize( Uint32 width, Uint32 height ) {
|
||||
}
|
||||
|
||||
void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
windowed = true;
|
||||
if ( ( !width || !height ) ) {
|
||||
width = mWindow.DesktopResolution.getWidth();
|
||||
height = mWindow.DesktopResolution.getHeight();
|
||||
@@ -747,7 +751,7 @@ void WindowSDL::minimize() {
|
||||
}
|
||||
|
||||
void WindowSDL::maximize() {
|
||||
if ( Runtime::isOffscreen() )
|
||||
if ( Runtime::isOffscreen() || Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_MaximizeWindow( mSDLWindow );
|
||||
}
|
||||
@@ -765,10 +769,14 @@ void WindowSDL::hide() {
|
||||
}
|
||||
|
||||
void WindowSDL::raise() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_RaiseWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
void WindowSDL::restore() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_RestoreWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
@@ -784,6 +792,8 @@ void WindowSDL::flash( WindowFlashOperation op ) {
|
||||
}
|
||||
|
||||
void WindowSDL::show() {
|
||||
if ( Runtime::forceHiddenWindows() )
|
||||
return;
|
||||
SDL_ShowWindow( mSDLWindow );
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/inifile.hpp>
|
||||
#include <eepp/system/luapattern.hpp>
|
||||
#include <eepp/system/packfileindex.hpp>
|
||||
#include <eepp/system/packregistry.hpp>
|
||||
#include <eepp/system/parsermatcher.hpp>
|
||||
#include <eepp/system/regex.hpp>
|
||||
#include <eepp/system/thread.hpp>
|
||||
#include <eepp/system/packfileindex.hpp>
|
||||
#include <eepp/ui/css/stylesheetspecification.hpp>
|
||||
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
|
||||
#include <eepp/ui/tools/uiinspectorserver.hpp>
|
||||
@@ -299,6 +299,10 @@ EE::Window::Window* Engine::createDefaultWindow( const WindowSettings& Settings,
|
||||
}
|
||||
|
||||
EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettings Context ) {
|
||||
if ( Runtime::forceHiddenWindows() ) {
|
||||
Settings.Style |= WindowStyle::Hidden;
|
||||
Settings.Style &= ~( WindowStyle::Fullscreen | WindowStyle::UseDesktopResolution );
|
||||
}
|
||||
const bool firstWindow = mWindows.empty();
|
||||
if ( Runtime::mode() == RuntimeMode::Terminal && !mWindows.empty() ) {
|
||||
Log::error( "Terminal runtime currently supports one top-level Window" );
|
||||
|
||||
@@ -29,6 +29,11 @@ bool Runtime::isOffscreen() {
|
||||
return mode() != RuntimeMode::Native;
|
||||
}
|
||||
|
||||
bool Runtime::forceHiddenWindows() {
|
||||
const char* value = std::getenv( "EEPP_WINDOW_HIDDEN" );
|
||||
return value && 0 == std::strcmp( value, "1" );
|
||||
}
|
||||
|
||||
const char* Runtime::modeName() {
|
||||
switch ( mode() ) {
|
||||
case RuntimeMode::Headless:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "utest.h"
|
||||
#include <eepp/system/scopedop.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/window/runtime.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::Window;
|
||||
|
||||
UTEST( Runtime, hiddenWindowOverrideRequiresOneAndPreservesRuntime ) {
|
||||
const std::string previous = Sys::getEnv( "EEPP_WINDOW_HIDDEN" );
|
||||
ScopedOp environment( nullptr,
|
||||
[&previous] { Sys::setEnv( "EEPP_WINDOW_HIDDEN", previous.c_str() ); } );
|
||||
const auto mode = Runtime::mode();
|
||||
for ( const char* value : { "", "0", "true", "false", "01", "1" } ) {
|
||||
EXPECT_TRUE( Sys::setEnv( "EEPP_WINDOW_HIDDEN", value ) );
|
||||
EXPECT_EQ( Runtime::forceHiddenWindows(), value[0] == '1' && value[1] == '\0' );
|
||||
EXPECT_EQ( Runtime::mode(), mode );
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( UIApplication, forcedHiddenWindowsIgnorePresentationRequests ) {
|
||||
if ( Runtime::mode() == RuntimeMode::Terminal )
|
||||
return;
|
||||
const std::string previous = Sys::getEnv( "EEPP_WINDOW_HIDDEN" );
|
||||
ScopedOp environment( [] { Sys::setEnv( "EEPP_WINDOW_HIDDEN", "1" ); },
|
||||
[&previous] { Sys::setEnv( "EEPP_WINDOW_HIDDEN", previous.c_str() ); } );
|
||||
WindowSettings settings{ 320, 240, "Forced Hidden Window Test" };
|
||||
// Do not inherit the unit harness's hidden default: the environment must force both windows
|
||||
// hidden and suppress the requested fullscreen desktop mode before native creation.
|
||||
settings.Style = WindowStyle::Titlebar | WindowStyle::Resize | WindowStyle::Fullscreen |
|
||||
WindowStyle::UseDesktopResolution;
|
||||
UIApplication app( settings );
|
||||
auto* secondary = app.createWindow( settings );
|
||||
ASSERT_TRUE( secondary != nullptr );
|
||||
for ( auto* window : { app.getWindow(), secondary->getWindow() } ) {
|
||||
EXPECT_TRUE( window->getWindowInfo()->WindowConfig.Style & WindowStyle::Hidden );
|
||||
EXPECT_FALSE( window->getWindowInfo()->WindowConfig.Style &
|
||||
( WindowStyle::Fullscreen | WindowStyle::UseDesktopResolution ) );
|
||||
EXPECT_FALSE( window->isVisible() );
|
||||
EXPECT_TRUE( window->isWindowed() );
|
||||
window->show();
|
||||
window->raise();
|
||||
window->restore();
|
||||
window->maximize();
|
||||
window->toggleFullscreen();
|
||||
window->setSize( 360, 260, false );
|
||||
EXPECT_FALSE( window->isVisible() );
|
||||
EXPECT_TRUE( window->isWindowed() );
|
||||
}
|
||||
}
|
||||
+37
-50
@@ -261,10 +261,21 @@ Tab Tab::close:hover,
|
||||
Tab:not(:selected) Tab::close:hover {
|
||||
foreground-tint: var(--tab-close-hover);
|
||||
}
|
||||
/* Scrollbars stay out of the way until used. */
|
||||
/* The focused pane uses the theme's thumb color; the others stay muted. */
|
||||
ScrollBar::vbutton, ScrollBar::hbutton {
|
||||
background-color: rgba( var(--font-hint), 0.45 );
|
||||
}
|
||||
.pane:focus-within ScrollBar::vbutton,
|
||||
.pane:focus-within ScrollBar::hbutton {
|
||||
background-color: var(--scrollbar-button);
|
||||
}
|
||||
/* Keep Breeze's hover color above the more specific focused-pane rule. */
|
||||
.pane:focus-within ScrollBar::vslider:hover ScrollBar::vbutton,
|
||||
.pane:focus-within ScrollBar::vbutton:hover,
|
||||
.pane:focus-within ScrollBar::hslider:hover ScrollBar::hbutton,
|
||||
.pane:focus-within ScrollBar::hbutton:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
.transfer_text {
|
||||
margin-right: 6dp;
|
||||
}
|
||||
@@ -617,28 +628,6 @@ bool App::init( const std::vector<std::string>& locations, bool forceSplit ) {
|
||||
} else if ( event->Type == InputEvent::EventsSent ) {
|
||||
if ( mFileOperations->hasDroppedFiles() )
|
||||
mFileOperations->dropExternalFiles();
|
||||
} else if ( event->Type == InputEvent::MouseButtonDown ) {
|
||||
// Any press inside a pane (or its location bar) makes it the active one. Widgets
|
||||
// handle their own clicks, so this is tracked at the window level.
|
||||
// The widget under the pointer must belong to the pane: menus and dialogs drawn over
|
||||
// it are not the pane.
|
||||
// Use the dispatcher's view coordinates: native button events can use logical
|
||||
// coordinates while the UI uses drawable pixels on high-DPI displays.
|
||||
Vector2f mouse = mApp->getWindow()->getInput()->getMousePosFromView(
|
||||
mApp->getWindow()->getDefaultView() );
|
||||
Node* over = mUI->overFind( mouse );
|
||||
for ( int i = 0; i < 2; ++i ) {
|
||||
Pane& p = mPaneManager->pane( i );
|
||||
if ( over && ( ( p.root->getParent() == mPaneSplitter &&
|
||||
( p.root == over || p.root->inParentTreeOf( over ) ) ) ||
|
||||
( p.location.box->isVisible() &&
|
||||
( p.location.box == over ||
|
||||
p.location.box->inParentTreeOf( over ) ) ) ) ) {
|
||||
if ( i != mPaneManager->activeIndex() )
|
||||
mPaneManager->setActivePane( i );
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ( event->Type == InputEvent::MouseButtonUp ) {
|
||||
// Side mouse buttons navigate the history.
|
||||
if ( event->button.button == EE_BUTTON_4 )
|
||||
@@ -654,6 +643,7 @@ bool App::init( const std::vector<std::string>& locations, bool forceSplit ) {
|
||||
return false;
|
||||
} );
|
||||
|
||||
// Keyboard focus determines the active pane; scrolling and background operations do not.
|
||||
// The keyboard belongs to the files: a click on a pane's bars or empty areas focuses the
|
||||
// container under the pointer, which would swallow the keys (typing, arrows, Enter). Hand
|
||||
// it to that pane's files on the next frame. Buttons and text fields keep it.
|
||||
@@ -671,31 +661,30 @@ bool App::init( const std::vector<std::string>& locations, bool forceSplit ) {
|
||||
}
|
||||
if ( !focus || !focus->isWidget() )
|
||||
return;
|
||||
for ( int i = 0; i < 2; ++i ) {
|
||||
auto* tab = mPaneManager->currentTab( mPaneManager->pane( i ) );
|
||||
if ( !tab || !mPaneManager->pane( i ).root->inParentTreeOf( focus ) )
|
||||
continue;
|
||||
// Row clicks focus a cell (a push button), not the view itself. Keep file commands
|
||||
// on that pane before leaving the focused control in charge of its own input.
|
||||
if ( i != mPaneManager->activeIndex() )
|
||||
mPaneManager->setActivePane( i );
|
||||
if ( focus->isType( UI_TYPE_TEXTINPUT ) || tab->view == focus ||
|
||||
tab->view->inParentTreeOf( focus ) )
|
||||
return;
|
||||
// Buttons and their parts (a tab's close button): the click that follows goes to
|
||||
// the focused node, it must stay there.
|
||||
for ( Node* node = focus; node; node = node->getParent() ) {
|
||||
if ( node->isType( UI_TYPE_PUSHBUTTON ) )
|
||||
return;
|
||||
}
|
||||
mUI->runOnMainThread( [this, i, focus] {
|
||||
auto* current = mPaneManager->currentTab( mPaneManager->pane( i ) );
|
||||
// Only if nothing else took the focus meanwhile (focus is not dereferenced).
|
||||
if ( current && mUI->getEventDispatcher()->getFocusNode() == focus )
|
||||
current->view->setFocus();
|
||||
} );
|
||||
Pane* pane = mPaneManager->paneForNode( focus );
|
||||
if ( !pane )
|
||||
return;
|
||||
auto* tab = mPaneManager->currentTab( *pane );
|
||||
if ( !tab )
|
||||
return;
|
||||
// Row clicks focus a cell (a push button), not the view itself. Refresh the pane's
|
||||
// decorations without replacing the focused control that must receive the click.
|
||||
mPaneManager->onFocusChange();
|
||||
if ( focus->isType( UI_TYPE_TEXTINPUT ) || tab->view == focus ||
|
||||
tab->view->inParentTreeOf( focus ) )
|
||||
return;
|
||||
// Buttons and their parts (a tab's close button): the click that follows goes to
|
||||
// the focused node, it must stay there.
|
||||
for ( Node* node = focus; node; node = node->getParent() ) {
|
||||
if ( node->isType( UI_TYPE_PUSHBUTTON ) )
|
||||
return;
|
||||
}
|
||||
mUI->runOnMainThread( [this, i = pane->index, focus] {
|
||||
auto* current = mPaneManager->currentTab( mPaneManager->pane( i ) );
|
||||
// Only if nothing else took the focus meanwhile (focus is not dereferenced).
|
||||
if ( current && mUI->getEventDispatcher()->getFocusNode() == focus )
|
||||
current->view->setFocus();
|
||||
} );
|
||||
} );
|
||||
|
||||
mPaneManager->restoreTabs( mState, locations, forceSplit );
|
||||
@@ -774,7 +763,7 @@ void App::setSplit( bool split ) {
|
||||
} else if ( !split && right.root->getParent() == mPaneSplitter ) {
|
||||
right.root->setParent( mParking );
|
||||
if ( mPaneManager->activeIndex() == 1 )
|
||||
mPaneManager->setActivePane( 0 );
|
||||
mPaneManager->focusPane( 0 );
|
||||
}
|
||||
right.location.box->setVisible( split );
|
||||
if ( auto* button = mUI->find<UISelectButton>( "split" ) )
|
||||
@@ -1002,9 +991,7 @@ void App::registerShortcuts() {
|
||||
mUI->setKeyBindingCommand( "switch-pane", fileCommand( [this] {
|
||||
if ( !mSplit )
|
||||
return;
|
||||
mPaneManager->setActivePane( 1 - mPaneManager->activeIndex() );
|
||||
if ( auto* tab = mPaneManager->currentTab() )
|
||||
tab->view->setFocus();
|
||||
mPaneManager->focusPane( 1 - mPaneManager->activeIndex() );
|
||||
} ) );
|
||||
mUI->setKeyBindingCommand( "refresh", onTab( []( FileTab* tab ) { tab->model->refresh(); } ) );
|
||||
mUI->setKeyBindingCommand( "go-back", [this] { mPaneManager->goBack(); } );
|
||||
|
||||
@@ -1094,7 +1094,7 @@ FileOperations::dropDestination( Vector2f position ) {
|
||||
if ( auto row = tab->view->rowOf( over );
|
||||
row && *row < tab->model->rowCount() && tab->model->entry( *row ).isDirectory() )
|
||||
folder = tab->model->entryURI( *row );
|
||||
mApp.panes().setActivePane( target );
|
||||
mApp.panes().focusPane( target );
|
||||
return DropDestination{ std::move( folder ), target };
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,12 @@ PaneManager::PaneManager( App& app ) : mApp( app ), mUI( app.ui() ) {}
|
||||
|
||||
PaneManager::~PaneManager() = default;
|
||||
|
||||
int PaneManager::activeIndex() const {
|
||||
if ( auto* focused = paneForNode( mUI->getEventDispatcher()->getFocusNode() ) )
|
||||
return focused->index;
|
||||
return mLastFocusedPane;
|
||||
}
|
||||
|
||||
void PaneManager::clearTabs() {
|
||||
for ( auto& pane : mPanes ) {
|
||||
if ( pane )
|
||||
@@ -141,7 +147,7 @@ void PaneManager::createPane( int index, UIWidget* parent ) {
|
||||
part->on( Event::MouseClick, [this, index]( const Event* event ) {
|
||||
if ( !( static_cast<const MouseEvent*>( event )->getFlags() & EE_BUTTON_RMASK ) )
|
||||
return;
|
||||
setActivePane( index );
|
||||
focusPane( index );
|
||||
if ( auto* tab = currentTab( pane( index ) ) )
|
||||
mApp.fileOps().showContextMenu( tab, true );
|
||||
} );
|
||||
@@ -215,7 +221,7 @@ void PaneManager::createPane( int index, UIWidget* parent ) {
|
||||
if ( mMovingTab && mMovingTab->tab == tabEvent->getTab() ) {
|
||||
mMovingTab->pane = &pane;
|
||||
pane.tabList.push_back( std::move( mMovingTab ) );
|
||||
setActivePane( index );
|
||||
focusPane( index );
|
||||
}
|
||||
positionNewTabButton( pane );
|
||||
} );
|
||||
@@ -236,7 +242,7 @@ void PaneManager::createPane( int index, UIWidget* parent ) {
|
||||
mApp.shortcutTooltip( mUI->i18n( "eefm_new_tab", "New Tab" ), "new-tab" ) );
|
||||
p.newTabButton->onClick( [this, index]( const MouseEvent* ) {
|
||||
Pane& pane = this->pane( index );
|
||||
setActivePane( index );
|
||||
focusPane( index );
|
||||
auto* tab = currentTab( pane );
|
||||
openTab( pane, tab ? tab->model->uri() : URI::fromFSPath( Sys::getUserDirectory() ) );
|
||||
} );
|
||||
@@ -252,8 +258,6 @@ void PaneManager::createPane( int index, UIWidget* parent ) {
|
||||
tab->view->setFocus();
|
||||
}
|
||||
} );
|
||||
p.location.input->on( Event::OnFocus,
|
||||
[this, index]( const Event* ) { setActivePane( index ); } );
|
||||
frame->findByClass( "location_clear" )->onClick( [this, index]( const MouseEvent* ) {
|
||||
auto* input = this->pane( index ).location.input;
|
||||
input->setText( "" );
|
||||
@@ -320,7 +324,6 @@ void PaneManager::createPane( int index, UIWidget* parent ) {
|
||||
applyFilter( tab );
|
||||
}
|
||||
} );
|
||||
p.filterInput->on( Event::OnFocus, [this, index]( const Event* ) { setActivePane( index ); } );
|
||||
// Escape returns to the files (the filter and the selection stay).
|
||||
p.filterInput->on( Event::KeyDown, [this, index]( const Event* event ) {
|
||||
if ( event->asKeyEvent()->getKeyCode() != KEY_ESCAPE )
|
||||
@@ -344,6 +347,11 @@ FileTab* PaneManager::currentTab( Pane& pane ) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FileTab* PaneManager::currentTab() {
|
||||
auto* owner = mPanes[activeIndex()].get();
|
||||
return owner ? currentTab( *owner ) : nullptr;
|
||||
}
|
||||
|
||||
FileTab* PaneManager::findTab( UITab* uitab ) {
|
||||
for ( auto& pane : mPanes ) {
|
||||
for ( auto& tab : pane->tabList ) {
|
||||
@@ -354,13 +362,40 @@ FileTab* PaneManager::findTab( UITab* uitab ) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PaneManager::setActivePane( int index ) {
|
||||
Pane* PaneManager::paneForNode( const Node* node ) const {
|
||||
if ( !node )
|
||||
return nullptr;
|
||||
for ( const auto& pane : mPanes ) {
|
||||
if ( !pane )
|
||||
continue;
|
||||
for ( auto* root : { pane->root, pane->location.box } ) {
|
||||
if ( root && ( root == node || root->isParentOf( node ) ) )
|
||||
return pane.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PaneManager::focusPane( int index ) {
|
||||
if ( index == 1 && !mApp.isSplit() )
|
||||
index = 0;
|
||||
mActivePane = index;
|
||||
Pane& target = pane( index );
|
||||
if ( paneForNode( mUI->getEventDispatcher()->getFocusNode() ) != &target ) {
|
||||
if ( auto* tab = currentTab( target ) )
|
||||
tab->view->setFocus();
|
||||
}
|
||||
// Startup and tab restoration can already have placed focus here before the tab was ready.
|
||||
onFocusChange();
|
||||
}
|
||||
|
||||
void PaneManager::onFocusChange() {
|
||||
Pane* focused = paneForNode( mUI->getEventDispatcher()->getFocusNode() );
|
||||
if ( !focused || ( mLastFocusedPane == focused->index && focused->root->hasClass( "active" ) ) )
|
||||
return;
|
||||
mLastFocusedPane = focused->index;
|
||||
for ( int i = 0; i < 2; ++i ) {
|
||||
Pane& p = pane( i );
|
||||
if ( i == index ) {
|
||||
if ( &p == focused ) {
|
||||
p.root->addClass( "active" );
|
||||
p.location.box->addClass( "active" );
|
||||
} else {
|
||||
@@ -368,7 +403,7 @@ void PaneManager::setActivePane( int index ) {
|
||||
p.location.box->removeClass( "active" );
|
||||
}
|
||||
}
|
||||
updateChrome( activePane() );
|
||||
updateChrome( *focused );
|
||||
}
|
||||
|
||||
void PaneManager::positionNewTabButton( Pane& target ) {
|
||||
@@ -447,7 +482,7 @@ FileTab* PaneManager::openTab( Pane& pane, const URI& uri, bool select ) {
|
||||
};
|
||||
view->on( Event::OnSizeChange, [capColumns]( const Event* ) { capColumns(); } );
|
||||
view->onModelEvent( [this, raw]( const ModelEvent* event ) {
|
||||
setActivePane( raw->pane->index );
|
||||
focusPane( raw->pane->index );
|
||||
if ( event->getModelEventType() == ModelEventType::Open ) {
|
||||
mApp.fileOps().openRow( raw, event->getModelIndex().row() );
|
||||
} else if ( event->getModelEventType() == ModelEventType::OpenMenu ) {
|
||||
@@ -460,7 +495,6 @@ FileTab* PaneManager::openTab( Pane& pane, const URI& uri, bool select ) {
|
||||
// Middle click opens a folder in a new tab, in the background (as Dolphin and browsers);
|
||||
// with the modifier (Ctrl, Cmd on macOS) the new tab is selected and focused.
|
||||
view->onRowMiddleClick = [this, raw]( size_t row ) {
|
||||
setActivePane( raw->pane->index );
|
||||
if ( row >= raw->model->rowCount() || !raw->model->entry( row ).isDirectory() )
|
||||
return;
|
||||
bool select =
|
||||
@@ -474,7 +508,7 @@ FileTab* PaneManager::openTab( Pane& pane, const URI& uri, bool select ) {
|
||||
updateStatus( *raw->pane );
|
||||
} );
|
||||
view->onBackgroundMenu = [this, raw] {
|
||||
setActivePane( raw->pane->index );
|
||||
focusPane( raw->pane->index );
|
||||
mApp.fileOps().showContextMenu( raw, true );
|
||||
};
|
||||
view->dragPayload = [this, raw] { return raw->selectedURIs(); };
|
||||
@@ -492,8 +526,6 @@ FileTab* PaneManager::openTab( Pane& pane, const URI& uri, bool select ) {
|
||||
mApp.fileOps().dropFiles( FileTableView::currentDrag().uris, destination,
|
||||
raw->pane->index );
|
||||
};
|
||||
view->on( Event::OnFocus, [this, raw]( const Event* ) { setActivePane( raw->pane->index ); } );
|
||||
|
||||
raw->tab = pane.tabs->add( Location::displayName( uri ), view, mApp.icon( "mime-folder" ) );
|
||||
// The new tab button would sit over a dragged tab: hidden while one moves (a press alone
|
||||
// starts a drag, so it waits for the movement), back in place after.
|
||||
@@ -646,7 +678,7 @@ void PaneManager::updateChrome( Pane& pane ) {
|
||||
if ( !pane.location.input->hasFocus() )
|
||||
pane.location.input->setText( Location::text( uri ) );
|
||||
updateStatus( pane );
|
||||
if ( pane.index != mActivePane )
|
||||
if ( pane.index != activeIndex() )
|
||||
return;
|
||||
mUI->find<UIWidget>( "back" )->setEnabled( tab->historyIndex > 0 );
|
||||
mUI->find<UIWidget>( "forward" )->setEnabled( tab->historyIndex + 1 < tab->history.size() );
|
||||
@@ -861,10 +893,8 @@ void PaneManager::restoreTabs( const nlohmann::json& state,
|
||||
}
|
||||
mApp.setSplit( split );
|
||||
// Start on the left pane, with the keyboard there too: restoring tabs focuses the views it
|
||||
// selects (the right pane's last), and the highlight alone does not move the focus.
|
||||
setActivePane( 0 );
|
||||
if ( auto* tab = currentTab( pane( 0 ) ) )
|
||||
tab->view->setFocus();
|
||||
// selects (the right pane's last).
|
||||
focusPane( 0 );
|
||||
}
|
||||
|
||||
} // namespace eefm
|
||||
|
||||
@@ -28,9 +28,10 @@ class PaneManager {
|
||||
|
||||
Pane& pane( int index ) { return *mPanes[index]; }
|
||||
|
||||
Pane& activePane() { return *mPanes[mActivePane]; }
|
||||
Pane& activePane() { return pane( activeIndex() ); }
|
||||
|
||||
int activeIndex() const { return mActivePane; }
|
||||
/** @return The pane owning keyboard focus, or the last focused pane while other UI owns it. */
|
||||
int activeIndex() const;
|
||||
|
||||
/** @return True once both panes exist. */
|
||||
bool ready() const { return mPanes[0] && mPanes[1]; }
|
||||
@@ -39,10 +40,13 @@ class PaneManager {
|
||||
|
||||
FileTab* currentTab( Pane& pane );
|
||||
|
||||
FileTab* currentTab() { return mPanes[mActivePane] ? currentTab( activePane() ) : nullptr; }
|
||||
FileTab* currentTab();
|
||||
|
||||
FileTab* findTab( UITab* tab );
|
||||
|
||||
/** @return The pane owning a node in its view or location bar, or nullptr for other UI. */
|
||||
Pane* paneForNode( const Node* node ) const;
|
||||
|
||||
FileTab* openTab( Pane& pane, const URI& uri, bool select = true );
|
||||
|
||||
void closeTab( FileTab* tab );
|
||||
@@ -50,7 +54,11 @@ class PaneManager {
|
||||
/** Closes every tab (their models stop listing and watching). */
|
||||
void clearTabs();
|
||||
|
||||
void setActivePane( int index );
|
||||
/** Gives a pane's files keyboard focus unless one of its controls already owns it. */
|
||||
void focusPane( int index );
|
||||
|
||||
/** Updates pane decorations and remembers the pane owning the current keyboard focus. */
|
||||
void onFocusChange();
|
||||
|
||||
void navigate( FileTab* tab, URI uri, bool recordHistory = true );
|
||||
|
||||
@@ -98,7 +106,9 @@ class PaneManager {
|
||||
std::unique_ptr<FileTab> mMovingTab;
|
||||
/** Column widths / visibility / sort shared by new tabs. */
|
||||
nlohmann::json mViewState;
|
||||
int mActivePane{ 0 };
|
||||
/** Command context while a toolbar, popup, dialog, places or terminal owns keyboard focus.
|
||||
* Only updated from actual pane focus; activeIndex() always prefers the current focus. */
|
||||
int mLastFocusedPane{ 0 };
|
||||
EE::VFS::VFSDirectoryModel::NameFilterMode mFilterMode{
|
||||
EE::VFS::VFSDirectoryModel::NameFilterMode::Glob };
|
||||
bool mFilterCaseSensitive{ false };
|
||||
|
||||
Reference in New Issue
Block a user