mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-18 19:43:24 +03:00
Add initial eproc system monitor
- Add the eproc process-monitoring application and persistent window/table state - Implement Linux process, network, GPU, icon, and GUI-window collection - Add optional process columns, styling, i18n-ready strings, and command copying - Retain exited processes for one update and color them as ended - Extend table sizing and header behavior for persisted column layouts - Organize Linux-specific sources under platform/linux - Keep unsupported platforms buildable with a startup WIP warning
This commit is contained in:
@@ -0,0 +1,527 @@
|
||||
#include "process_model.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <eepp/graphics/glyphdrawable.hpp>
|
||||
#include <eepp/graphics/image.hpp>
|
||||
#include <eepp/graphics/pixeldensity.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
#if EE_PLATFORM != EE_PLATFORM_WIN
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace eproc {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* processColumnClass( size_t column ) {
|
||||
switch ( column ) {
|
||||
case ProcessModel::ColIcon:
|
||||
return "eproc-process-column-icon";
|
||||
case ProcessModel::ColName:
|
||||
return "eproc-process-column-name";
|
||||
case ProcessModel::ColPid:
|
||||
return "eproc-process-column-pid";
|
||||
case ProcessModel::ColUsername:
|
||||
return "eproc-process-column-username";
|
||||
case ProcessModel::ColCpu:
|
||||
return "eproc-process-column-cpu";
|
||||
case ProcessModel::ColMemory:
|
||||
return "eproc-process-column-memory";
|
||||
case ProcessModel::ColSharedMem:
|
||||
return "eproc-process-column-shared-memory";
|
||||
case ProcessModel::ColGpuUsage:
|
||||
return "eproc-process-column-gpu-usage";
|
||||
case ProcessModel::ColGpuMemory:
|
||||
return "eproc-process-column-gpu-memory";
|
||||
case ProcessModel::ColDownload:
|
||||
return "eproc-process-column-download";
|
||||
case ProcessModel::ColUpload:
|
||||
return "eproc-process-column-upload";
|
||||
case ProcessModel::ColCommand:
|
||||
return "eproc-process-column-command";
|
||||
case ProcessModel::ColTotalMemory:
|
||||
return "eproc-process-column-total-memory";
|
||||
case ProcessModel::ColVirtualSize:
|
||||
return "eproc-process-column-virtual-size";
|
||||
case ProcessModel::ColCpuTime:
|
||||
return "eproc-process-column-cpu-time";
|
||||
case ProcessModel::ColNiceness:
|
||||
return "eproc-process-column-niceness";
|
||||
case ProcessModel::ColRelativeStartTime:
|
||||
return "eproc-process-column-relative-start-time";
|
||||
case ProcessModel::ColTty:
|
||||
return "eproc-process-column-tty";
|
||||
case ProcessModel::ColIoRead:
|
||||
return "eproc-process-column-io-read";
|
||||
case ProcessModel::ColIoWrite:
|
||||
return "eproc-process-column-io-write";
|
||||
default:
|
||||
return "eproc-process-column-base";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProcessModel::ProcessModel( UISceneNode* ui ) : mUI( ui ) {}
|
||||
|
||||
ProcessModel::~ProcessModel() {}
|
||||
|
||||
size_t ProcessModel::rowCount( const ModelIndex& ) const {
|
||||
return mFilteredProcesses.size();
|
||||
}
|
||||
|
||||
size_t ProcessModel::columnCount( const ModelIndex& ) const {
|
||||
return ColCount;
|
||||
}
|
||||
|
||||
std::string ProcessModel::columnName( const size_t& column ) const {
|
||||
switch ( column ) {
|
||||
case ColIcon:
|
||||
return "";
|
||||
case ColName:
|
||||
return mUI->i18n( "eproc_column_name", "Name" ).toUtf8();
|
||||
case ColPid:
|
||||
return mUI->i18n( "eproc_column_pid", "PID" ).toUtf8();
|
||||
case ColUsername:
|
||||
return mUI->i18n( "eproc_column_username", "Username" ).toUtf8();
|
||||
case ColCpu:
|
||||
return mUI->i18n( "eproc_column_cpu", "CPU %" ).toUtf8();
|
||||
case ColMemory:
|
||||
return mUI->i18n( "eproc_column_memory", "Memory" ).toUtf8();
|
||||
case ColSharedMem:
|
||||
return mUI->i18n( "eproc_column_shared_memory", "Shared Mem" ).toUtf8();
|
||||
case ColGpuUsage:
|
||||
return mUI->i18n( "eproc_column_gpu_usage", "GPU Usage" ).toUtf8();
|
||||
case ColGpuMemory:
|
||||
return mUI->i18n( "eproc_column_gpu_memory", "GPU Memory" ).toUtf8();
|
||||
case ColDownload:
|
||||
return mUI->i18n( "eproc_column_download", "Download" ).toUtf8();
|
||||
case ColUpload:
|
||||
return mUI->i18n( "eproc_column_upload", "Upload" ).toUtf8();
|
||||
case ColCommand:
|
||||
return mUI->i18n( "eproc_column_command", "Command" ).toUtf8();
|
||||
case ColTotalMemory:
|
||||
return mUI->i18n( "eproc_column_total_memory", "Total Memory" ).toUtf8();
|
||||
case ColVirtualSize:
|
||||
return mUI->i18n( "eproc_column_virtual_size", "Virtual Size" ).toUtf8();
|
||||
case ColCpuTime:
|
||||
return mUI->i18n( "eproc_column_cpu_time", "CPU Time" ).toUtf8();
|
||||
case ColNiceness:
|
||||
return mUI->i18n( "eproc_column_niceness", "Niceness" ).toUtf8();
|
||||
case ColRelativeStartTime:
|
||||
return mUI->i18n( "eproc_column_relative_start_time", "Relative Start Time" ).toUtf8();
|
||||
case ColTty:
|
||||
return mUI->i18n( "eproc_column_tty", "TTY" ).toUtf8();
|
||||
case ColIoRead:
|
||||
return mUI->i18n( "eproc_column_io_read", "IO Read" ).toUtf8();
|
||||
case ColIoWrite:
|
||||
return mUI->i18n( "eproc_column_io_write", "IO Write" ).toUtf8();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Variant ProcessModel::data( const ModelIndex& index, ModelRole role ) const {
|
||||
const std::string EMPTY = "";
|
||||
|
||||
if ( role == ModelRole::Class ) {
|
||||
const char* cls = processColumnClass( index.column() );
|
||||
return cls ? Variant( cls ) : Variant();
|
||||
}
|
||||
|
||||
if ( role == ModelRole::Icon ) {
|
||||
// The icon lives in its own column so that every icon lines up, instead of padding the
|
||||
// name text.
|
||||
const auto* proc = getProcessByRow( index.row() );
|
||||
if ( !proc || index.column() != ColIcon || proc->iconPath.empty() )
|
||||
return Variant();
|
||||
return Variant( iconFor( proc->iconPath ) );
|
||||
}
|
||||
|
||||
if ( role == ModelRole::Sort ) {
|
||||
const auto* proc = getProcessByRow( index.row() );
|
||||
if ( !proc )
|
||||
return Variant();
|
||||
switch ( index.column() ) {
|
||||
case ColPid:
|
||||
return Variant( static_cast<Int64>( proc->pid ) );
|
||||
case ColCpu:
|
||||
return Variant( static_cast<Int64>( proc->getCpuForSort() ) );
|
||||
case ColMemory:
|
||||
return Variant( static_cast<Int64>( proc->getMemoryForSort() ) );
|
||||
case ColSharedMem:
|
||||
return Variant( static_cast<Int64>( proc->sharedMem ) );
|
||||
case ColGpuUsage:
|
||||
return Variant( static_cast<Int64>( proc->gpuUsage ) );
|
||||
case ColGpuMemory:
|
||||
return Variant( static_cast<Int64>( proc->gpuMemory ) );
|
||||
case ColDownload:
|
||||
return Variant( static_cast<Int64>( proc->netDownload ) );
|
||||
case ColUpload:
|
||||
return Variant( static_cast<Int64>( proc->netUpload ) );
|
||||
case ColTotalMemory:
|
||||
return Variant( static_cast<Int64>( proc->vmRSS ) );
|
||||
case ColVirtualSize:
|
||||
return Variant( static_cast<Int64>( proc->vmSize ) );
|
||||
case ColCpuTime:
|
||||
return Variant( static_cast<Int64>( proc->userTime ) + proc->sysTime );
|
||||
case ColNiceness:
|
||||
return Variant( static_cast<Int64>( proc->niceLevel ) );
|
||||
case ColRelativeStartTime:
|
||||
return Variant( static_cast<Int64>( proc->startTime ) );
|
||||
case ColTty:
|
||||
return Variant( static_cast<Int64>( proc->ttyNr ) );
|
||||
case ColIoRead:
|
||||
return Variant( static_cast<Int64>( proc->ioReadBytes ) );
|
||||
case ColIoWrite:
|
||||
return Variant( static_cast<Int64>( proc->ioWriteBytes ) );
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( role != ModelRole::Display )
|
||||
return Variant();
|
||||
|
||||
const auto* proc = getProcessByRow( index.row() );
|
||||
if ( !proc )
|
||||
return Variant();
|
||||
|
||||
switch ( index.column() ) {
|
||||
case ColName:
|
||||
return Variant( proc->name );
|
||||
case ColPid:
|
||||
return Variant( String::toString( static_cast<Int64>( proc->pid ) ) );
|
||||
case ColUsername:
|
||||
return Variant( proc->username );
|
||||
case ColCpu: {
|
||||
std::string cpu = proc->formatCpu();
|
||||
return Variant( cpu );
|
||||
}
|
||||
case ColMemory:
|
||||
return Variant( proc->formatMemory() );
|
||||
case ColSharedMem:
|
||||
return Variant( proc->formatSharedMem() );
|
||||
case ColGpuUsage:
|
||||
return Variant( proc->formatGpuUsage() );
|
||||
case ColGpuMemory:
|
||||
return Variant( proc->formatGpuMemory() );
|
||||
case ColDownload:
|
||||
return Variant( proc->formatDownload() );
|
||||
case ColUpload:
|
||||
return Variant( proc->formatUpload() );
|
||||
case ColCommand:
|
||||
return Variant( proc->command );
|
||||
case ColTotalMemory:
|
||||
return Variant( formatKiB( proc->vmRSS ) );
|
||||
case ColVirtualSize:
|
||||
return Variant( formatKiB( proc->vmSize ) );
|
||||
case ColCpuTime:
|
||||
return Variant( proc->formatCpuTime( mSystemInfo.clockTicksPerSecond ) );
|
||||
case ColNiceness:
|
||||
return Variant( String::toString( static_cast<Int64>( proc->niceLevel ) ) );
|
||||
case ColRelativeStartTime:
|
||||
return Variant( proc->formatRelativeStartTime( mSystemInfo.uptimeSeconds,
|
||||
mSystemInfo.clockTicksPerSecond ) );
|
||||
case ColTty:
|
||||
return Variant( proc->tty );
|
||||
case ColIoRead:
|
||||
return Variant( formatBytes( proc->ioReadBytes ) );
|
||||
case ColIoWrite:
|
||||
return Variant( formatBytes( proc->ioWriteBytes ) );
|
||||
default:
|
||||
return Variant( EMPTY );
|
||||
}
|
||||
}
|
||||
|
||||
ModelIndex ProcessModel::index( int row, int column, const ModelIndex& ) const {
|
||||
return createIndex( row, column );
|
||||
}
|
||||
|
||||
void ProcessModel::applySnapshot( std::vector<ProcessInfo>&& processes,
|
||||
const SystemInfo& sysInfo ) {
|
||||
// Keep processes that disappeared from the latest snapshot for one more update. This mirrors
|
||||
// ksysguard's Ended state and is especially useful when a process exits between two refreshes:
|
||||
// its last known row remains visible, but is marked as ended by the view.
|
||||
UnorderedMap<long, long long> currentStartTimes;
|
||||
currentStartTimes.reserve( processes.size() );
|
||||
for ( const auto& process : processes )
|
||||
currentStartTimes[process.pid] = process.startTime;
|
||||
|
||||
std::vector<ProcessInfo> endedProcesses;
|
||||
endedProcesses.reserve( mProcesses.size() );
|
||||
for ( auto& previous : mProcesses ) {
|
||||
// An ended process was already shown during the previous update. Do not keep it for a
|
||||
// second update.
|
||||
if ( previous.status == ProcessStatus::Ended )
|
||||
continue;
|
||||
|
||||
auto current = currentStartTimes.find( previous.pid );
|
||||
const bool processStillExists = current != currentStartTimes.end() &&
|
||||
( previous.startTime == 0 || current->second == 0 ||
|
||||
previous.startTime == current->second );
|
||||
if ( processStillExists )
|
||||
continue;
|
||||
|
||||
previous.status = ProcessStatus::Ended;
|
||||
endedProcesses.emplace_back( std::move( previous ) );
|
||||
}
|
||||
|
||||
processes.reserve( processes.size() + endedProcesses.size() );
|
||||
for ( auto& process : endedProcesses )
|
||||
processes.emplace_back( std::move( process ) );
|
||||
|
||||
mProcesses = std::move( processes );
|
||||
mSystemInfo = sysInfo;
|
||||
applyFilters();
|
||||
onModelUpdate();
|
||||
}
|
||||
|
||||
void ProcessModel::setFilter( FilterMode mode ) {
|
||||
mFilterMode = mode;
|
||||
applyFilters();
|
||||
onModelUpdate();
|
||||
}
|
||||
|
||||
void ProcessModel::setTextFilter( const std::string& text ) {
|
||||
mTextRegex.reset();
|
||||
mTextLiteral.clear();
|
||||
|
||||
if ( !text.empty() ) {
|
||||
// Compiled here rather than per row: the filter runs over every process on every pass.
|
||||
// useCache is false because the pattern changes on each keystroke: the cache is
|
||||
// least-recently-used and shared with the syntax definitions, so caching every typed prefix
|
||||
// would evict the patterns the tokenizer is working from.
|
||||
// AllowFallback keeps the default engine behaviour: a pattern PCRE2 rejects is retried
|
||||
// with Oniguruma before it is treated as invalid.
|
||||
auto regex = std::make_unique<RegEx>(
|
||||
text, RegEx::Options::Utf | RegEx::Options::AllowFallback | RegEx::Options::Caseless,
|
||||
false );
|
||||
if ( regex->isValid() ) {
|
||||
mTextRegex = std::move( regex );
|
||||
} else {
|
||||
// An unfinished pattern matches nothing, which would blank the table while the user is
|
||||
// still typing it, so it is searched for literally instead.
|
||||
mTextLiteral = text;
|
||||
std::transform( mTextLiteral.begin(), mTextLiteral.end(), mTextLiteral.begin(),
|
||||
[]( unsigned char c ) { return std::tolower( c ); } );
|
||||
}
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
onModelUpdate();
|
||||
}
|
||||
|
||||
// Case-insensitive substring test that does not allocate.
|
||||
static bool containsIgnoreCase( std::string_view haystack, std::string_view lowerNeedle ) {
|
||||
if ( lowerNeedle.empty() )
|
||||
return true;
|
||||
if ( haystack.size() < lowerNeedle.size() )
|
||||
return false;
|
||||
|
||||
for ( size_t i = 0; i + lowerNeedle.size() <= haystack.size(); ++i ) {
|
||||
size_t j = 0;
|
||||
for ( ; j < lowerNeedle.size(); ++j ) {
|
||||
if ( std::tolower( static_cast<unsigned char>( haystack[i + j] ) ) != lowerNeedle[j] )
|
||||
break;
|
||||
}
|
||||
if ( j == lowerNeedle.size() )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcessModel::matchesText( const ProcessInfo& proc ) const {
|
||||
if ( !mTextRegex && mTextLiteral.empty() )
|
||||
return true;
|
||||
|
||||
// The original lets the user search by PID too. The digits are formatted into a stack buffer
|
||||
// because this runs for every process on every pass while a filter is active.
|
||||
char pidBuffer[24];
|
||||
int pidLength = snprintf( pidBuffer, sizeof( pidBuffer ), "%ld", proc.pid );
|
||||
size_t pidSize = pidLength > 0 ? static_cast<size_t>( pidLength ) : 0;
|
||||
|
||||
if ( mTextRegex ) {
|
||||
// Each column is matched on its own, so an anchored pattern applies to every one of them.
|
||||
return mTextRegex->matches( proc.name ) || mTextRegex->matches( proc.command ) ||
|
||||
mTextRegex->matches( proc.username ) ||
|
||||
mTextRegex->matches( pidBuffer, 0, nullptr, pidSize );
|
||||
}
|
||||
|
||||
const std::string_view pid( pidBuffer, pidSize );
|
||||
return containsIgnoreCase( proc.name, mTextLiteral ) ||
|
||||
containsIgnoreCase( proc.command, mTextLiteral ) ||
|
||||
containsIgnoreCase( proc.username, mTextLiteral ) ||
|
||||
containsIgnoreCase( pid, mTextLiteral );
|
||||
}
|
||||
|
||||
bool ProcessModel::accepts( const ProcessInfo& proc ) const {
|
||||
switch ( mFilterMode ) {
|
||||
case AllProcesses:
|
||||
return true;
|
||||
|
||||
case SystemProcesses:
|
||||
// System accounts are those below uid 100, or accounts that cannot log in.
|
||||
return proc.uid < 100 || !proc.canLogin;
|
||||
|
||||
case UserProcesses:
|
||||
// Keep a process when either its real or effective id belongs to a login-capable
|
||||
// account, mirroring the original's paired check.
|
||||
return ( proc.uid >= 100 && proc.canLogin ) ||
|
||||
( proc.euid >= 100 && proc.euidCanLogin );
|
||||
|
||||
case OwnProcesses: {
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
const long own = -1;
|
||||
#else
|
||||
const long own = static_cast<long>( getuid() );
|
||||
#endif
|
||||
return proc.uid == own || proc.euid == own || proc.suid == own || proc.fsuid == own;
|
||||
}
|
||||
|
||||
case ProgramsOnly: {
|
||||
// A "program" owns a terminal or a GUI window. login/getty *are* the tty rather than
|
||||
// something started from it, so the original hides them as well.
|
||||
if ( proc.ttyNr == 0 && mGuiPids.count( proc.pid ) == 0 )
|
||||
return false;
|
||||
if ( proc.parentPid == 1 &&
|
||||
( proc.name == "login" ||
|
||||
proc.name.compare( std::max<int>( 0, (int)proc.name.size() - 5 ), 5, "getty" ) ==
|
||||
0 ) )
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessModel::applyFilters() {
|
||||
mFilteredProcesses.clear();
|
||||
mFilteredProcesses.reserve( mProcesses.size() );
|
||||
|
||||
for ( auto& proc : mProcesses ) {
|
||||
if ( !accepts( proc ) || !matchesText( proc ) )
|
||||
continue;
|
||||
|
||||
mFilteredProcesses.push_back( &proc );
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessModel::setGuiWindowPids( UnorderedSet<long>&& pids ) {
|
||||
mGuiPids = std::move( pids );
|
||||
}
|
||||
|
||||
// nanosvg substitutes its own default (white) fill for the SVG constructs it cannot parse, so an
|
||||
// unsupported icon rasterizes to a blank white square. Such a result is reported as "no icon"
|
||||
// rather than drawn as a misleading box.
|
||||
static bool isBlankIcon( const Image& image ) {
|
||||
const Uint8* pixels = image.getPixelsPtr();
|
||||
const unsigned int channels = image.getChannels();
|
||||
const unsigned int width = image.getWidth();
|
||||
const unsigned int height = image.getHeight();
|
||||
|
||||
if ( !pixels || width == 0 || height == 0 || channels < 3 )
|
||||
return false; // no pixel access: trust the image rather than dropping the icon
|
||||
|
||||
for ( unsigned int y = 0; y < height; ++y ) {
|
||||
for ( unsigned int x = 0; x < width; ++x ) {
|
||||
const Uint8* pixel = pixels + ( static_cast<size_t>( y ) * width + x ) * channels;
|
||||
if ( channels == 4 && pixel[3] < 8 )
|
||||
continue; // fully transparent
|
||||
if ( pixel[0] < 240 || pixel[1] < 240 || pixel[2] < 240 )
|
||||
return false; // real content
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The original renders 16px icons in the name column.
|
||||
static constexpr int kIconSizeDp = 16;
|
||||
|
||||
DrawablePtr ProcessModel::iconFor( const std::string& path ) const {
|
||||
auto it = mIconCache.find( path );
|
||||
if ( it != mIconCache.end() )
|
||||
return it->second;
|
||||
|
||||
// Loading is done once per icon file: the table asks for this on every cell refresh, and
|
||||
// re-loading the texture each time would be pathological.
|
||||
const Uint32 iconPx = static_cast<Uint32>( PixelDensity::dpToPxI( kIconSizeDp ) );
|
||||
Image image;
|
||||
|
||||
if ( String::endsWith( path, ".svg" ) || String::endsWith( path, ".svgz" ) ) {
|
||||
// Scalable icons are rasterized before resizing, so the same high-quality image resampler
|
||||
// is used for both SVG and bitmap icons.
|
||||
std::ifstream file( path, std::ios::binary );
|
||||
std::string svg( ( std::istreambuf_iterator<char>( file ) ),
|
||||
std::istreambuf_iterator<char>() );
|
||||
if ( !svg.empty() ) {
|
||||
Image::FormatConfiguration format;
|
||||
int width = 0, height = 0, channels = 0;
|
||||
if ( Image::getInfoFromMemory( reinterpret_cast<const unsigned char*>( svg.data() ),
|
||||
svg.size(), &width, &height, &channels, format ) ) {
|
||||
format.svgScale( iconPx / static_cast<Float>( eemax( width, height ) ) );
|
||||
image = Image( reinterpret_cast<const Uint8*>( svg.data() ),
|
||||
static_cast<unsigned int>( svg.size() ), 4, format );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
image = Image( path, 4 );
|
||||
}
|
||||
|
||||
// Decode at the final pixel size and use Lanczos resampling for bitmap icons. This avoids
|
||||
// relying on the GPU's texture filtering to reduce large process icons at draw time.
|
||||
if ( image.getPixelsPtr() && image.getWidth() > 0 && image.getHeight() > 0 ) {
|
||||
if ( image.getWidth() != iconPx || image.getHeight() != iconPx )
|
||||
image.resize( iconPx, iconPx, Image::RESAMPLER_LANCZOS4 );
|
||||
|
||||
if ( isBlankIcon( image ) )
|
||||
image = Image();
|
||||
}
|
||||
|
||||
TexturePtr texture;
|
||||
if ( image.getPixelsPtr() && image.getWidth() > 0 && image.getHeight() > 0 ) {
|
||||
texture = TextureFactory::instance()->loadFromPixels(
|
||||
image.getPixelsPtr(), image.getWidth(), image.getHeight(), image.getChannels(), false,
|
||||
Texture::ClampMode::ClampToEdge, false, false, path );
|
||||
}
|
||||
|
||||
// The table draws a ModelRole::Icon drawable at its own size, so the icon is built already
|
||||
// scaled to the row height; at native resolution it would overflow the row.
|
||||
DrawablePtr drawable;
|
||||
if ( texture ) {
|
||||
// GlyphDrawable feeds the source rect to quadsSetTexCoord as u/v, so the rect is
|
||||
// normalized: (0,0,1,1) selects the whole texture.
|
||||
Rect srcRect( 0, 0, 1, 1 );
|
||||
auto* glyph = GlyphDrawable::New( texture, srcRect, Sizef( iconPx, iconPx ), path );
|
||||
glyph->setDrawMode( GlyphDrawable::DrawMode::Image );
|
||||
glyph->setGlyphRenderMode( GlyphRenderMode::Color );
|
||||
glyph->setPixelDensity( PixelDensity::getPixelDensity() );
|
||||
drawable = DrawablePtr( glyph );
|
||||
}
|
||||
|
||||
mIconCache.emplace( path, drawable );
|
||||
return drawable;
|
||||
}
|
||||
|
||||
const ProcessInfo* ProcessModel::getProcessByRow( int row ) const {
|
||||
if ( row < 0 || static_cast<size_t>( row ) >= mFilteredProcesses.size() )
|
||||
return nullptr;
|
||||
return mFilteredProcesses[row];
|
||||
}
|
||||
|
||||
int ProcessModel::rowForPid( long pid ) const {
|
||||
for ( size_t i = 0; i < mFilteredProcesses.size(); ++i ) {
|
||||
if ( mFilteredProcesses[i]->pid == pid )
|
||||
return static_cast<int>( i );
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace eproc
|
||||
Reference in New Issue
Block a user