diff --git a/bin/assets/icon/ecode-icon-web.hvif b/bin/assets/icon/ecode-icon-circular.hvif similarity index 100% rename from bin/assets/icon/ecode-icon-web.hvif rename to bin/assets/icon/ecode-icon-circular.hvif diff --git a/bin/assets/icon/ecode-icon-web.svg b/bin/assets/icon/ecode-icon-circular.svg similarity index 100% rename from bin/assets/icon/ecode-icon-web.svg rename to bin/assets/icon/ecode-icon-circular.svg diff --git a/bin/assets/icon/ecode-icon.svg b/bin/assets/icon/ecode-icon.svg index 06efdc0ad..cf67a0631 100644 --- a/bin/assets/icon/ecode-icon.svg +++ b/bin/assets/icon/ecode-icon.svg @@ -1,107 +1,26 @@ - - + + + + + + + - - - - - - - - - - - - - - - - + + + + + + diff --git a/bin/assets/icon/ecode.icns b/bin/assets/icon/ecode.icns index 3424b0ae8..eec56d8b5 100644 Binary files a/bin/assets/icon/ecode.icns and b/bin/assets/icon/ecode.icns differ diff --git a/bin/assets/icon/ecode.ico b/bin/assets/icon/ecode.ico index 9700d471f..5e68a8bc4 100644 Binary files a/bin/assets/icon/ecode.ico and b/bin/assets/icon/ecode.ico differ diff --git a/bin/assets/icon/ecode.png b/bin/assets/icon/ecode.png index f02eeb34f..90de07453 100644 Binary files a/bin/assets/icon/ecode.png and b/bin/assets/icon/ecode.png differ diff --git a/bin/assets/icon/ecode.res b/bin/assets/icon/ecode.res index 44cb82844..2651167b7 100644 Binary files a/bin/assets/icon/ecode.res and b/bin/assets/icon/ecode.res differ diff --git a/bin/assets/icon/ecode.x64.res b/bin/assets/icon/ecode.x64.res index 6bcfd26b6..c589f3f97 100644 Binary files a/bin/assets/icon/ecode.x64.res and b/bin/assets/icon/ecode.x64.res differ diff --git a/bin/assets/icon/ee-icon-web.svg b/bin/assets/icon/ee-icon-circular.svg similarity index 100% rename from bin/assets/icon/ee-icon-web.svg rename to bin/assets/icon/ee-icon-circular.svg diff --git a/bin/assets/icon/ee-icon.svg b/bin/assets/icon/ee-icon.svg index af26ed5c6..555f5ea73 100644 --- a/bin/assets/icon/ee-icon.svg +++ b/bin/assets/icon/ee-icon.svg @@ -1,107 +1,26 @@ - - + + + + + + + - - - - - - - - - - - - - - - - + + + + + + diff --git a/bin/assets/icon/ee.icns b/bin/assets/icon/ee.icns index 552807452..10f8c7863 100644 Binary files a/bin/assets/icon/ee.icns and b/bin/assets/icon/ee.icns differ diff --git a/bin/assets/icon/ee.ico b/bin/assets/icon/ee.ico index 820acbeca..cd7ea8ca5 100644 Binary files a/bin/assets/icon/ee.ico and b/bin/assets/icon/ee.ico differ diff --git a/bin/assets/icon/ee.png b/bin/assets/icon/ee.png index 27dc5c735..1fa33f182 100644 Binary files a/bin/assets/icon/ee.png and b/bin/assets/icon/ee.png differ diff --git a/bin/assets/icon/ee.res b/bin/assets/icon/ee.res index 7f3ccefb7..e012e94b5 100644 Binary files a/bin/assets/icon/ee.res and b/bin/assets/icon/ee.res differ diff --git a/bin/assets/icon/ee.x64.res b/bin/assets/icon/ee.x64.res index 014fdfc7f..5e0695497 100644 Binary files a/bin/assets/icon/ee.x64.res and b/bin/assets/icon/ee.x64.res differ diff --git a/bin/assets/icon/eeiv-icon.svg b/bin/assets/icon/eeiv-icon.svg new file mode 100644 index 000000000..d27b2d225 --- /dev/null +++ b/bin/assets/icon/eeiv-icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/assets/icon/eeiv.png b/bin/assets/icon/eeiv.png new file mode 100644 index 000000000..1f0714ef5 Binary files /dev/null and b/bin/assets/icon/eeiv.png differ diff --git a/bin/assets/icon/eproc-icon.svg b/bin/assets/icon/eproc-icon.svg new file mode 100644 index 000000000..6f5576636 --- /dev/null +++ b/bin/assets/icon/eproc-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/bin/assets/icon/eproc.png b/bin/assets/icon/eproc.png new file mode 100644 index 000000000..7ba90af7b Binary files /dev/null and b/bin/assets/icon/eproc.png differ diff --git a/bin/assets/icon/eterm-icon-web.svg b/bin/assets/icon/eterm-icon-circular.svg similarity index 100% rename from bin/assets/icon/eterm-icon-web.svg rename to bin/assets/icon/eterm-icon-circular.svg diff --git a/bin/assets/icon/eterm-icon.svg b/bin/assets/icon/eterm-icon.svg index 00aa80a3e..4d60abb41 100644 --- a/bin/assets/icon/eterm-icon.svg +++ b/bin/assets/icon/eterm-icon.svg @@ -1,107 +1,21 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/bin/assets/icon/eterm.icns b/bin/assets/icon/eterm.icns index 89494e198..0f5217bac 100644 Binary files a/bin/assets/icon/eterm.icns and b/bin/assets/icon/eterm.icns differ diff --git a/bin/assets/icon/eterm.ico b/bin/assets/icon/eterm.ico index 5fa990c6a..25dd610d0 100644 Binary files a/bin/assets/icon/eterm.ico and b/bin/assets/icon/eterm.ico differ diff --git a/bin/assets/icon/eterm.png b/bin/assets/icon/eterm.png index 495c4412c..6ec015ca2 100644 Binary files a/bin/assets/icon/eterm.png and b/bin/assets/icon/eterm.png differ diff --git a/bin/assets/icon/eterm.res b/bin/assets/icon/eterm.res index d16ac3c0d..144d61860 100644 Binary files a/bin/assets/icon/eterm.res and b/bin/assets/icon/eterm.res differ diff --git a/bin/assets/icon/eterm.x64.res b/bin/assets/icon/eterm.x64.res index 72518d727..47c3f88b0 100644 Binary files a/bin/assets/icon/eterm.x64.res and b/bin/assets/icon/eterm.x64.res differ diff --git a/include/eepp/ui/abstract/uiabstracttableview.hpp b/include/eepp/ui/abstract/uiabstracttableview.hpp index 035b34f16..7cb2154f9 100644 --- a/include/eepp/ui/abstract/uiabstracttableview.hpp +++ b/include/eepp/ui/abstract/uiabstracttableview.hpp @@ -15,6 +15,7 @@ namespace EE { namespace UI { class UIPushButton; class UILinearLayout; class UIDropDownModelList; +class UIPopUpMenu; }} // namespace EE::UI namespace EE { namespace UI { namespace Abstract { @@ -60,6 +61,20 @@ class EE_API UIAbstractTableView : public UIAbstractView { void setColumnsHidden( const std::vector& columns, bool hidden ); + /** Enables dragging column headers to reorder them. Disabled by default. */ + void setColumnReorderingEnabled( bool enabled ); + + bool isColumnReorderingEnabled() const; + + /** Model column IDs in left-to-right display order, including hidden columns. */ + const std::vector& getColumnOrder() const; + + /** Sets a complete permutation of the current model's column IDs. Returns false if invalid. */ + bool setColumnOrder( std::vector order ); + + /** Moves a model column to a position in the complete display order. */ + bool moveColumn( size_t column, size_t position ); + virtual void selectAll(); virtual std::vector getSelectionRange( const ModelIndex& start, @@ -100,6 +115,11 @@ class EE_API UIAbstractTableView : public UIAbstractView { void setColumnWidthModeMenuEnabled( bool enabled ); + /** Lets a view append application-specific items to a column header's context menu. */ + void setOnHeaderContextMenuCb( std::function callback ) { + mOnHeaderContextMenuCb = std::move( callback ); + } + void setColumnWidthPercentage( const size_t& colIndex, Float percentage ); void setColumnsWidthPercentage( const std::vector& percentages ); @@ -226,6 +246,8 @@ class EE_API UIAbstractTableView : public UIAbstractView { Float mHeaderHeight{ 16 }; mutable std::vector mRows; mutable std::vector mColumn; + // Visual order only. ColumnData and ModelIndex remain keyed by model column ID. + std::vector mColumnOrder; mutable std::vector> mWidgets; UILinearLayout* mHeader{ nullptr }; UILinearLayout* mRowHeader{ nullptr }; @@ -243,10 +265,12 @@ class EE_API UIAbstractTableView : public UIAbstractView { std::unordered_map> mWidgetsClickCbId; std::function mOnUpdateCellCb; std::function mSetupCellCb; + std::function mOnHeaderContextMenuCb; Float mRowHeaderWidth{ 0 }; Uint32 mTableFlags{ UITABLE_DEFAULT_FLAGS }; ColumnWidthMode mColumnWidthMode{ ColumnWidthMode::Pixels }; bool mColumnWidthModeMenuEnabled{ false }; + bool mColumnReorderingEnabled{ false }; bool mUpdatingColumnsForScrollbars{ false }; bool mAutoExpandedColumnUsesVerticalScroll{ false }; std::string mPendingSerializedColumnWidths; @@ -260,6 +284,10 @@ class EE_API UIAbstractTableView : public UIAbstractView { ColumnData& columnData( const size_t& column ) const; + void applyColumnOrder(); + + void reorderColumnAt( size_t column, Float centerX ); + virtual size_t getItemCount() const; virtual void onModelUpdate( unsigned flags ); diff --git a/include/eepp/ui/uitableheadercolumn.hpp b/include/eepp/ui/uitableheadercolumn.hpp index 53135ebe9..df90893b6 100644 --- a/include/eepp/ui/uitableheadercolumn.hpp +++ b/include/eepp/ui/uitableheadercolumn.hpp @@ -21,6 +21,10 @@ class EE_API UITableHeaderColumn : public UIPushButton { UIAbstractTableView* mView; size_t mColIndex; mutable UIImage* mImage{ nullptr }; + enum class DragMode : Uint8 { None, Resize, ReorderPending, Reorder }; + DragMode mDragMode{ DragMode::None }; + Float mReorderGrabX{ 0 }; + Vector2f mReorderPressPos; Uint32 onCalculateDrag( const Vector2f& position, const Uint32& flags ); diff --git a/premake4.lua b/premake4.lua index 0dceaed5d..5d6211abf 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1978,10 +1978,12 @@ solution "eepp" "src/tools/eproc/process_collector.cpp", "src/tools/eproc/process_info.cpp", "src/tools/eproc/process_model.cpp", + "src/tools/eproc/window_icon.cpp", } if os.is_real("linux") then files { "src/tools/eproc/platform/linux/gpu_reader_nvidia.cpp", + "src/tools/eproc/platform/linux/gpu_reader_drm.cpp", "src/tools/eproc/platform/linux/process_collector_linux.cpp", "src/tools/eproc/platform/linux/process_icon_resolver.cpp", "src/tools/eproc/platform/linux/process_network_monitor.cpp", @@ -2028,6 +2030,9 @@ solution "eepp" links { "bsd", "network" } end files { "src/tests/unit_tests/*.cpp", + "src/tools/eproc/process_info.cpp", + "src/tools/eproc/process_model.cpp", + "src/tools/eproc/window_icon.cpp", "src/tools/ecode/ignorematcher.cpp", "src/tools/ecode/jsonhelper.cpp", "src/tools/ecode/projectdirectorytree.cpp", diff --git a/premake5.lua b/premake5.lua index bdd93d531..d9274c93b 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1999,10 +1999,12 @@ workspace "eepp" "src/tools/eproc/process_collector.cpp", "src/tools/eproc/process_info.cpp", "src/tools/eproc/process_model.cpp", + "src/tools/eproc/window_icon.cpp", } filter "system:linux" files { "src/tools/eproc/platform/linux/gpu_reader_nvidia.cpp", + "src/tools/eproc/platform/linux/gpu_reader_drm.cpp", "src/tools/eproc/platform/linux/process_collector_linux.cpp", "src/tools/eproc/platform/linux/process_icon_resolver.cpp", "src/tools/eproc/platform/linux/process_network_monitor.cpp", @@ -2043,6 +2045,9 @@ workspace "eepp" incdirs { "src/modules/eterm/include/", "src/thirdparty" } language "C++" files { "src/tests/unit_tests/*.cpp", + "src/tools/eproc/process_info.cpp", + "src/tools/eproc/process_model.cpp", + "src/tools/eproc/window_icon.cpp", "src/tools/ecode/ignorematcher.cpp", "src/tools/ecode/jsonhelper.cpp", "src/tools/ecode/projectdirectorytree.cpp", diff --git a/projects/scripts/generate_app_icons.py b/projects/scripts/generate_app_icons.py new file mode 100755 index 000000000..056324737 --- /dev/null +++ b/projects/scripts/generate_app_icons.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""Regenerate the application icons in bin/assets/icon from their SVG sources. + +For every application this derives the 256x256 .png runtime window icon and, +where platform packaging needs them, the Windows .ico, the MinGW .res/.x64.res +COFF resource objects, and the macOS .icns container. + +The binary layouts intentionally mirror the historical artifacts: + + .ico 32bpp BMP entries (bottom-up BGRA XOR bitmap + AND mask with the bit + set where alpha == 0) for sizes <= 128, a PNG entry for 256, with + each application's historical entry sizes and order preserved. + .icns ic04/ic05 as 'ARGB' magic + per-plane RLE (literal byte n < 0x80 + introduces n+1 raw bytes, repeat byte n >= 0x80 introduces + (n & 0x7f) + 3 copies of the next byte), PNG payloads for the + larger types, and the 'info' metadata chunk carried over from the + original Apple iconutil-generated files. + .res compiled from .rc + .ico with mingw windres, for both the + pe-i386 and pe-x86-64 targets. + +Requires: python3 (stdlib only), inkscape, and a mingw-w64 windres (found as +$WINDRES, x86_64-w64-mingw32-windres, or windres). +""" + +import argparse +import base64 +import os +import shutil +import struct +import subprocess +import sys +import tempfile +import zlib + +PNG_SIZE = 256 + +# Sizes requested from the .icns entry table below (plus the ICO sizes and +# PNG_SIZE), rendered natively from the SVG instead of resampled. +ICNS_ENTRIES = [ + ( 'ic12', 64, 'png' ), + ( 'ic07', 128, 'png' ), + ( 'ic13', 256, 'png' ), + ( 'ic08', 256, 'png' ), + ( 'ic04', 16, 'argb' ), + ( 'ic14', 512, 'png' ), + ( 'ic09', 512, 'png' ), + ( 'ic05', 32, 'argb' ), + ( 'ic10', 1024, 'png' ), + ( 'ic11', 32, 'png' ), + ( 'info', 0, 'info' ), +] + +# The 'info' element is an NSKeyedArchiver property list written by Apple's +# iconutil. It carries no artwork, so the original bytes are kept verbatim to +# stay byte-compatible with iconutil output. +INFO_CHUNK = base64.b64decode( + "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8Q" + "D05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGnCwwXGBkaHlUkbnVsbNMNDg8QExZXTlMua2V5c1pO" + "Uy5vYmplY3RzViRjbGFzc6IREoACgAOiFBWABIAFgAZUbmFtZV8QFmFzc2V0Y2F0YWxvZy1yZWZl" + "cmVuY2VUaWNvbtMNDg8bHBagoIAG0h8gISJaJGNsYXNzbmFtZVgkY2xhc3Nlc1xOU0RpY3Rpb25h" + "cnmiISNYTlNPYmplY3QIERokKTI3SUxRU1thaHB7goWHiYyOkJKXsLW8vb7AxdDZ5ukAAAAAAAAB" + "AQAAAAAAAAAkAAAAAAAAAAAAAAAAAAAA8g==" +) + +# Per-application inputs and outputs. The ICO size lists keep the historical +# entry sets and order of each file; Windows selects the best size match +# regardless of order, so this is preserved only to keep artifact diffs minimal. +APPS = { + 'ee': { 'svg': 'ee-icon.svg', 'ico': [16, 24, 32, 48, 64, 128, 256], 'icns': True, 'res': True }, + 'ecode': { 'svg': 'ecode-icon.svg', 'ico': [256, 128, 64, 48, 32, 16], 'icns': True, 'res': True }, + 'eterm': { 'svg': 'eterm-icon.svg', 'ico': [16, 24, 32, 48, 64, 72, 96, 128, 256], 'icns': True, 'res': True }, + 'eeiv': { 'svg': 'eeiv-icon.svg', 'ico': [], 'icns': False, 'res': False }, + 'eproc': { 'svg': 'eproc-icon.svg', 'ico': [], 'icns': False, 'res': False }, +} + + +def decode_png( blob ): + """Decode an 8-bit RGBA non-interlaced PNG into row-major RGBA bytes.""" + + if blob[:8] != b'\x89PNG\r\n\x1a\n': + raise SystemExit( 'not a PNG blob' ) + width = height = 0 + idat = [] + pos = 8 + while pos + 8 <= len( blob ): + length, ctype = struct.unpack( '>I4s', blob[pos:pos + 8] ) + body = blob[pos + 8:pos + 8 + length] + if ctype == b'IHDR': + width, height, depth, color, _, _, interlace = struct.unpack( '>IIBBBBB', body ) + if ( depth, color, interlace ) != ( 8, 6, 0 ): + raise SystemExit( + f'unsupported PNG: depth={depth} color={color} interlace={interlace}' ) + elif ctype == b'IDAT': + idat.append( body ) + pos += 12 + length + raw = zlib.decompress( b''.join( idat ) ) + stride = width * 4 + out = bytearray( height * stride ) + previous = bytearray( stride ) + source = 0 + for y in range( height ): + filter_type = raw[source] + source += 1 + line = bytearray( raw[source:source + stride] ) + source += stride + for x in range( stride ): + left = line[x - 4] if x >= 4 else 0 + above = previous[x] + corner = previous[x - 4] if x >= 4 else 0 + if filter_type == 1: + line[x] = ( line[x] + left ) & 0xFF + elif filter_type == 2: + line[x] = ( line[x] + above ) & 0xFF + elif filter_type == 3: + line[x] = ( line[x] + ( left + above ) // 2 ) & 0xFF + elif filter_type == 4: + estimate = left + above - corner + distances = ( abs( estimate - left ), abs( estimate - above ), abs( estimate - corner ) ) + predictor = ( left, above, corner )[distances.index( min( distances ) )] + line[x] = ( line[x] + predictor ) & 0xFF + out[y * stride:( y + 1 ) * stride] = line + previous = line + return width, height, bytes( out ) + + +def bmp_entry( png ): + """32bpp BGRA bottom-up XOR bitmap plus AND mask, as in the historical .ico files.""" + + width, height, pixels = decode_png( png ) + xor = bytearray() + mask_rows = ( ( width + 31 ) // 32 ) * 4 + mask = bytearray() + for y in range( height - 1, -1, -1 ): + for x in range( width ): + offset = ( y * width + x ) * 4 + r, g, b, a = pixels[offset:offset + 4] + xor += bytes( ( b, g, r, a ) ) + row = bytearray( mask_rows ) + for x in range( width ): + if pixels[( y * width + x ) * 4 + 3] == 0: + row[x // 8] |= 0x80 >> ( x % 8 ) + mask += row + header = struct.pack( '= 0x80 introduces ( n & 0x7f ) + 3 copies.""" + + out = bytearray() + i = 0 + size = len( plane ) + while i < size: + run = 1 + while run < 130 and i + run < size and plane[i + run] == plane[i]: + run += 1 + if run >= 3: + out += bytes( ( 0x80 | ( run - 3 ), plane[i] ) ) + i += run + continue + start = i + i += run + while i - start < 128: + if i >= size: + break + run = 1 + while run < 3 and i + run < size and plane[i + run] == plane[i]: + run += 1 + if run >= 3: + break + i += run + out += bytes( ( i - start - 1, ) ) + bytes( plane[start:i] ) + return bytes( out ) + + +def rle_decode( stream ): + out = bytearray() + i = 0 + while i < len( stream ): + token = stream[i] + i += 1 + if token & 0x80: + out += bytes( [stream[i]] ) * ( ( token & 0x7f ) + 3 ) + i += 1 + else: + count = token + 1 + out += stream[i:i + count] + i += count + return bytes( out ) + + +def argb_entry( png ): + """ic04/ic05 payload: 'ARGB' magic plus RLE-compressed A, R, G, B planes.""" + + width, height, pixels = decode_png( png ) + planes = [bytearray( width * height ) for _ in range( 4 )] + for p in range( width * height ): + r, g, b, a = pixels[p * 4:p * 4 + 4] + planes[0][p] = a + planes[1][p] = r + planes[2][p] = g + planes[3][p] = b + encoded = [rle_encode( plane ) for plane in planes] + blob = b'ARGB' + b''.join( encoded ) + if rle_decode( blob[4:] ) != b''.join( planes ): + raise SystemExit( 'ARGB RLE round-trip failed' ) + return blob + + +def build_icns( pngs ): + chunks = [] + for entry_type, size, encoding in ICNS_ENTRIES: + if encoding == 'png': + payload = pngs[size] + elif encoding == 'argb': + payload = argb_entry( pngs[size] ) + else: + payload = INFO_CHUNK + chunks.append( entry_type.encode() + struct.pack( '>I', 8 + len( payload ) ) + payload ) + body = b''.join( chunks ) + return b'icns' + struct.pack( '>I', 8 + len( body ) ) + body + + +def render_png( inkscape, svg, size, out_path ): + subprocess.run( + [inkscape, '--export-type=png', '--export-area-page', + f'--export-width={size}', f'--export-height={size}', + '--export-background-opacity=0', f'--export-filename={out_path}', svg], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) + with open( out_path, 'rb' ) as handle: + return handle.read() + + +def find_windres(): + candidates = [os.environ.get( 'WINDRES' ), 'x86_64-w64-mingw32-windres', 'windres'] + for candidate in candidates: + if candidate and shutil.which( candidate ): + return candidate + raise SystemExit( 'windres not found (install mingw-w64 binutils or set $WINDRES)' ) + + +def generate( app, icon_dir, tmp_dir, inkscape, windres ): + spec = APPS[app] + icns_sizes = {size for _, size, _ in ICNS_ENTRIES} + needed = set( spec['ico'] ) | {PNG_SIZE} + if spec['icns']: + needed |= icns_sizes + pngs = {} + for size in sorted( needed ): + pngs[size] = render_png( inkscape, os.path.join( icon_dir, spec['svg'] ), size, + os.path.join( tmp_dir, f'{app}-{size}.png' ) ) + + written = [f'{app}.png'] + with open( os.path.join( icon_dir, f'{app}.png' ), 'wb' ) as handle: + handle.write( pngs[PNG_SIZE] ) + if spec['ico']: + with open( os.path.join( icon_dir, f'{app}.ico' ), 'wb' ) as handle: + handle.write( build_ico( pngs, spec['ico'] ) ) + written.append( f'{app}.ico' ) + if spec['icns']: + with open( os.path.join( icon_dir, f'{app}.icns' ), 'wb' ) as handle: + handle.write( build_icns( pngs ) ) + written.append( f'{app}.icns' ) + if spec['res']: + for target, output in ( ( 'pe-i386', f'{app}.res' ), ( 'pe-x86-64', f'{app}.x64.res' ) ): + subprocess.run( [windres, f'--target={target}', '-O', 'coff', f'{app}.rc', output], + check=True, cwd=icon_dir ) + written.append( f'{app}.res, {app}.x64.res' ) + print( f'{app}: {", ".join( written )}' ) + + +def main(): + parser = argparse.ArgumentParser( description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter ) + parser.add_argument( '--app', action='append', choices=sorted( APPS ), + help='application to regenerate (repeatable; default: all)' ) + args = parser.parse_args() + + script_dir = os.path.dirname( os.path.abspath( __file__ ) ) + icon_dir = os.path.join( os.path.abspath( os.path.join( script_dir, '..', '..' ) ), + 'bin', 'assets', 'icon' ) + inkscape = shutil.which( 'inkscape' ) + if not inkscape: + raise SystemExit( 'inkscape not found' ) + apps = args.app or list( APPS ) + windres = find_windres() if any( APPS[app]['res'] for app in apps ) else None + + with tempfile.TemporaryDirectory() as tmp_dir: + for app in apps: + generate( app, icon_dir, tmp_dir, inkscape, windres ) + return 0 + + +if __name__ == '__main__': + sys.exit( main() ) diff --git a/src/eepp/ui/abstract/uiabstracttableview.cpp b/src/eepp/ui/abstract/uiabstracttableview.cpp index 75724c23a..0a7e5db7f 100644 --- a/src/eepp/ui/abstract/uiabstracttableview.cpp +++ b/src/eepp/ui/abstract/uiabstracttableview.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -15,6 +16,43 @@ namespace EE { namespace UI { namespace Abstract { static constexpr String::HashType onModelUpdateTag = String::hash( "onModelUpdate" ); +class TableHeaderLayout : public UILinearLayout { + public: + TableHeaderLayout( const std::string& tag ) : + UILinearLayout( tag, UIOrientation::Horizontal ) {} + + void updateLayout() override { + UITableHeaderColumn* dragged = nullptr; + Vector2f dragPosition; + for ( Node* child = getFirstChild(); child; child = child->getNextNode() ) { + auto* header = static_cast( child ); + if ( header->isDragging() ) { + dragged = header; + dragPosition = header->getPixelsPosition(); + break; + } + } + UILinearLayout::updateLayout(); + if ( dragged ) + dragged->setPixelsPosition( dragPosition ); + } + + protected: + void drawChildren() override { + Node* dragged = nullptr; + for ( Node* child = getFirstChild(); child; child = child->getNextNode() ) { + if ( static_cast( child )->isDragging() ) { + dragged = child; + continue; + } + if ( child->isVisible() ) + child->nodeDraw(); + } + if ( dragged && dragged->isVisible() ) + dragged->nodeDraw(); + } +}; + template static void consumeVariantText( const Variant& value, Callback&& cb ) { switch ( value.getType() ) { case Variant::Type::String: @@ -43,7 +81,7 @@ UIAbstractTableView::UIAbstractTableView( const std::string& tag ) : mDragBorderDistance( PixelDensity::dpToPx( 4 ) ), mIconSize( PixelDensity::dpToPxI( 12 ) ), mSortIconSize( PixelDensity::dpToPxI( 20 ) ) { - mHeader = UILinearLayout::NewWithTag( mTag + "::header", UIOrientation::Horizontal ); + mHeader = eeNew( TableHeaderLayout, ( mTag + "::header" ) ); mHeader->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); mHeader->setParent( this )->setVisible( true )->setEnabled( true ); mHeader->setUpdateLayoutEvenIfNotVisible( true ); @@ -289,11 +327,13 @@ void UIAbstractTableView::restorePendingColumnWidths() { } void UIAbstractTableView::selectAll() { - getSelection().clear(); - for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) { - auto index = getModel()->index( itemIndex ); - getSelection().add( index ); - } + if ( !getModel() ) + return; + std::vector indexes; + indexes.reserve( getItemCount() ); + for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) + indexes.push_back( getModel()->index( itemIndex ) ); + getSelection().set( indexes ); } std::vector UIAbstractTableView::getSelectionRange( const ModelIndex& start, @@ -351,6 +391,18 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) { return; size_t count = model->columnCount(); + bool orderChanged = mColumnOrder.size() != count; + if ( orderChanged ) { + mColumnOrder.erase( std::remove_if( mColumnOrder.begin(), mColumnOrder.end(), + [count]( size_t column ) { return column >= count; } ), + mColumnOrder.end() ); + mColumnOrder.reserve( count ); + for ( size_t column = 0; column < count; ++column ) { + if ( std::find( mColumnOrder.begin(), mColumnOrder.end(), column ) == + mColumnOrder.end() ) + mColumnOrder.push_back( column ); + } + } Float totalWidth = 0; auto visibleColCount = visibleColumnCount(); @@ -360,6 +412,7 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) { for ( size_t i = 0; i < count; i++ ) { ColumnData& col = columnData( i ); if ( !col.widget ) { + orderChanged = true; col.widget = eeNew( UITableHeaderColumn, ( mTag, this, i ) ); col.widget->setParent( mHeader ); col.widget->setEnabled( true ); @@ -461,6 +514,8 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) { } } } + if ( orderChanged ) + applyColumnOrder(); mHeader->setPixelsSize( totalWidth, getHeaderHeight() ); bool visible = mHeader->isVisible(); @@ -596,12 +651,16 @@ void UIAbstractTableView::updatePercentageColumnWidths() { int UIAbstractTableView::adjacentVisibleColumn( size_t column ) const { if ( !getModel() ) return -1; - for ( size_t i = column + 1; i < getModel()->columnCount(); ++i ) - if ( !isColumnHidden( i ) ) - return i; - for ( size_t i = column; i > 0; --i ) - if ( !isColumnHidden( i - 1 ) ) - return i - 1; + auto current = std::find( mColumnOrder.begin(), mColumnOrder.end(), column ); + if ( current == mColumnOrder.end() ) + return -1; + for ( auto it = current + 1; it != mColumnOrder.end(); ++it ) + if ( !isColumnHidden( *it ) ) + return static_cast( *it ); + for ( auto it = current; it != mColumnOrder.begin(); ) { + if ( !isColumnHidden( *--it ) ) + return static_cast( *it ); + } return -1; } @@ -754,6 +813,91 @@ void UIAbstractTableView::setColumnsHidden( const std::vector& columns, createOrUpdateColumns( false ); } +void UIAbstractTableView::setColumnReorderingEnabled( bool enabled ) { + mColumnReorderingEnabled = enabled; +} + +bool UIAbstractTableView::isColumnReorderingEnabled() const { + return mColumnReorderingEnabled; +} + +const std::vector& UIAbstractTableView::getColumnOrder() const { + return mColumnOrder; +} + +bool UIAbstractTableView::setColumnOrder( std::vector order ) { + if ( !getModel() || order.size() != getModel()->columnCount() ) + return false; + std::vector seen( order.size(), false ); + for ( size_t column : order ) { + if ( column >= order.size() || seen[column] ) + return false; + seen[column] = true; + } + if ( order == mColumnOrder ) + return true; + mColumnOrder = std::move( order ); + applyColumnOrder(); + return true; +} + +bool UIAbstractTableView::moveColumn( size_t column, size_t position ) { + if ( position >= mColumnOrder.size() ) + return false; + auto from = std::find( mColumnOrder.begin(), mColumnOrder.end(), column ); + if ( from == mColumnOrder.end() ) + return false; + auto to = mColumnOrder.begin() + position; + if ( from == to ) + return false; + if ( from < to ) + std::rotate( from, from + 1, to + 1 ); + else + std::rotate( to, from, from + 1 ); + applyColumnOrder(); + return true; +} + +void UIAbstractTableView::applyColumnOrder() { + for ( size_t position = 0; position < mColumnOrder.size(); ++position ) { + auto* header = columnData( mColumnOrder[position] ).widget; + if ( header ) + header->toPosition( static_cast( position ) ); + } + mHeader->updateLayout(); + invalidateDraw(); +} + +void UIAbstractTableView::reorderColumnAt( size_t column, Float centerX ) { + auto found = std::find( mColumnOrder.begin(), mColumnOrder.end(), column ); + if ( found == mColumnOrder.end() ) + return; + const size_t current = static_cast( found - mColumnOrder.begin() ); + size_t target = current; + for ( size_t i = current; i > 0; --i ) { + const auto& previous = columnData( mColumnOrder[i - 1] ); + if ( !previous.visible || !previous.widget ) + continue; + const Float midpoint = previous.widget->getPixelsPosition().x + previous.width * 0.5f; + if ( centerX >= midpoint ) + break; + target = i - 1; + } + if ( target == current ) { + for ( size_t i = current + 1; i < mColumnOrder.size(); ++i ) { + const auto& next = columnData( mColumnOrder[i] ); + if ( !next.visible || !next.widget ) + continue; + const Float midpoint = next.widget->getPixelsPosition().x + next.width * 0.5f; + if ( centerX <= midpoint ) + break; + target = i; + } + } + if ( target != current ) + moveColumn( column, target ); +} + void UIAbstractTableView::setColumnsVisible( const std::vector& columns ) { if ( !getModel() ) return; @@ -819,8 +963,18 @@ UITableRow* UIAbstractTableView::createRow() { EE_BUTTON_RMASK ) ) return; auto index = event->getNode()->asType()->getCurIndex(); - if ( mSelectionKind == SelectionKind::Single && - ( getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) ) { + if ( event->asMouseEvent()->getFlags() & EE_BUTTON_RMASK ) { + bool selectedRow = false; + for ( const auto& selected : getSelection().indexes() ) { + if ( selected.row() == index.row() && selected.parent() == index.parent() ) { + selectedRow = true; + break; + } + } + if ( !selectedRow ) + getSelection().set( index ); + } else if ( mSelectionKind == SelectionKind::Single && + ( getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) ) { getSelection().remove( index ); } else { if ( mSelectionKind == SelectionKind::Multiple && diff --git a/src/eepp/ui/models/sortingproxymodel.cpp b/src/eepp/ui/models/sortingproxymodel.cpp index a89e49e26..a94920edd 100644 --- a/src/eepp/ui/models/sortingproxymodel.cpp +++ b/src/eepp/ui/models/sortingproxymodel.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -183,6 +182,28 @@ SortOrder SortingProxyModel::sortOrder() const { } void SortingProxyModel::sort( const size_t& column, const SortOrder& sortOrder ) { + struct ViewSelection { + UIAbstractView* view; + std::vector sourceIndexes; + }; + auto removeInvalid = []( std::vector& indexes ) { + indexes.erase( std::remove_if( indexes.begin(), indexes.end(), + []( const ModelIndex& index ) { return !index.isValid(); } ), + indexes.end() ); + }; + std::vector selections; + forEachView( [&]( UIAbstractView* view ) { + if ( view->getSelection().isEmpty() ) + return; + auto selected = view->getSelection().indexes(); + if ( selected.empty() ) + return; + for ( auto& index : selected ) + index = mapToSource( index ); + removeInvalid( selected ); + selections.push_back( { view, std::move( selected ) } ); + } ); + for ( auto& it : mMappings ) { auto& mapping = *it.second; sortMapping( mapping, column, sortOrder ); @@ -191,6 +212,14 @@ void SortingProxyModel::sort( const size_t& column, const SortOrder& sortOrder ) mKeyColumn = column; mSortOrder = sortOrder; + for ( auto& selection : selections ) { + for ( auto& index : selection.sourceIndexes ) + index = mapToProxy( index ); + removeInvalid( selection.sourceIndexes ); + selection.view->getSelection().set( selection.sourceIndexes, false ); + selection.view->notifySelectionChange(); + } + onModelUpdate( UpdateFlag::DontInvalidateIndexes ); } @@ -240,8 +269,6 @@ void SortingProxyModel::sortMapping( SortingProxyModel::Mapping& mapping, int co return; } - auto oldSourceRows = mapping.sourceRows; - int rowCount = source().rowCount( mapping.sourceParent ); for ( int i = 0; i < rowCount; ++i ) mapping.sourceRows[i] = i; @@ -256,36 +283,6 @@ void SortingProxyModel::sortMapping( SortingProxyModel::Mapping& mapping, int co for ( int i = 0; i < rowCount; ++i ) mapping.proxyRows[mapping.sourceRows[i]] = i; - - // FIXME: I really feel like this should be done at the view layer somehow. - forEachView( [&]( UIAbstractView* view ) { - // Update the view's selection. - view->getSelection().changeFromModel( [&]( ModelSelection& selection ) { - std::vector selectedIndexesInSource; - std::vector staleIndexesInSelection; - selection.forEachIndex( [&]( const ModelIndex& index ) { - if ( index.parent() == mapping.sourceParent ) { - staleIndexesInSelection.push_back( index ); - selectedIndexesInSource.push_back( source().index( - oldSourceRows[index.row()], index.column(), mapping.sourceParent ) ); - } - } ); - - for ( auto& index : staleIndexesInSelection ) - selection.remove( index ); - - for ( auto& index : selectedIndexesInSource ) { - for ( size_t i = 0; i < mapping.sourceRows.size(); ++i ) { - if ( mapping.sourceRows[i] == index.row() ) { - auto newSourceIndex = - this->index( i, index.column(), mapping.sourceParent ); - selection.add( newSourceIndex ); - break; - } - } - } - } ); - } ); } }}} // namespace EE::UI::Models diff --git a/src/eepp/ui/uitableheadercolumn.cpp b/src/eepp/ui/uitableheadercolumn.cpp index ceee8c712..0bd02cab8 100644 --- a/src/eepp/ui/uitableheadercolumn.cpp +++ b/src/eepp/ui/uitableheadercolumn.cpp @@ -1,8 +1,10 @@ #include +#include #include #include #include #include +#include namespace EE { namespace UI { @@ -60,10 +62,16 @@ Uint32 UITableHeaderColumn::onMouseDown( const Vector2i& position, const Uint32& Vector2f localPos( convertToNodeSpace( position.asFloat() ) ); if ( NULL != getEventDispatcher() && !getEventDispatcher()->isNodeDragging() && !( getEventDispatcher()->getLastPressTrigger() & mDragButton ) && - ( flags & mDragButton ) && isDragEnabled() && !isDragging() && - localPos.x >= mSize.getWidth() - mView->getDragBorderDistance() ) { - setFocus(); - startDragging( position.asFloat() ); + ( flags & mDragButton ) && isDragEnabled() && !isDragging() ) { + if ( localPos.x >= mSize.getWidth() - mView->getDragBorderDistance() ) { + mDragMode = DragMode::Resize; + setFocus(); + startDragging( position.asFloat() ); + } else if ( mView->isColumnReorderingEnabled() ) { + mDragMode = DragMode::ReorderPending; + mReorderGrabX = localPos.x; + mReorderPressPos = position.asFloat(); + } } pushState( UIState::StatePressed ); return Node::onMouseDown( position, flags ); @@ -81,6 +89,8 @@ Uint32 UITableHeaderColumn::onMouseClick( const Vector2i& position, const Uint32 } Uint32 UITableHeaderColumn::onMouseUp( const Vector2i& position, const Uint32& flags ) { + if ( flags & mDragButton && mDragMode == DragMode::ReorderPending ) + mDragMode = DragMode::None; if ( ( flags & EE_BUTTON_RMASK ) && mView->isColumnWidthModeMenuEnabled() && mView->getModel() ) { auto* menu = UIPopUpMenu::New(); @@ -157,6 +167,8 @@ Uint32 UITableHeaderColumn::onMouseUp( const Vector2i& position, const Uint32& f view->setColumnWidthMode( UIAbstractTableView::ColumnWidthMode::Pixels ); } } ); + if ( mView->mOnHeaderContextMenuCb ) + mView->mOnHeaderContextMenuCb( menu, mColIndex ); menu->setCloseOnHide( true ); menu->showAtScreenPosition( position.asFloat() ); } @@ -165,6 +177,15 @@ Uint32 UITableHeaderColumn::onMouseUp( const Vector2i& position, const Uint32& f Uint32 UITableHeaderColumn::onDrag( const Vector2f& position, const Uint32&, const Sizef& dragDiff ) { + if ( mDragMode == DragMode::Reorder ) { + Vector2f headerPos( mView->mHeader->convertToNodeSpace( position ) ); + const Float left = headerPos.x - mReorderGrabX; + mView->reorderColumnAt( mColIndex, left + mSize.getWidth() * 0.5f ); + setPixelsPosition( left, getPixelsPosition().y ); + return 1; + } + if ( mDragMode != DragMode::Resize ) + return 0; Vector2f localPos( convertToNodeSpace( position ) ); if ( isDragging() || localPos.x >= mSize.getWidth() - mView->getDragBorderDistance() ) { const Float width = eemax( mSize.x - dragDiff.x, mView->columnData( mColIndex ).minWidth ); @@ -180,12 +201,22 @@ Uint32 UITableHeaderColumn::onDrag( const Vector2f& position, const Uint32&, } Uint32 UITableHeaderColumn::onMouseLeave( const Vector2i& position, const Uint32& flags ) { + if ( mDragMode == DragMode::ReorderPending && getInput() && + ( getInput()->getPressTrigger() & mDragButton ) ) { + mDragMode = DragMode::Reorder; + startDragging( mReorderPressPos ); + } if ( !isDragging() ) getUISceneNode()->setCursor( Cursor::Arrow ); return UIPushButton::onMouseLeave( position, flags ); } Uint32 UITableHeaderColumn::onMouseMove( const Vector2i& position, const Uint32& flags ) { + if ( mDragMode == DragMode::ReorderPending && ( flags & mDragButton ) && + std::abs( position.x - mReorderPressPos.x ) >= mView->getDragBorderDistance() ) { + mDragMode = DragMode::Reorder; + startDragging( mReorderPressPos ); + } Vector2f localPos( convertToNodeSpace( position.asFloat() ) ); if ( isDragging() || localPos.x >= mSize.getWidth() - mView->getDragBorderDistance() ) { getUISceneNode()->setCursor( Cursor::SizeWE ); @@ -204,9 +235,14 @@ Uint32 UITableHeaderColumn::onMouseDoubleClick( const Vector2i& position, const Uint32 UITableHeaderColumn::onDragStop( const Vector2i& pos, const Uint32& flags ) { getUISceneNode()->setCursor( Cursor::Arrow ); - mView->columnData( mColIndex ).setWidth( mSize.getWidth(), true ); - mView->updateHeaderSize(); - mView->onColumnSizeChange( mColIndex, true ); + if ( mDragMode == DragMode::Resize ) { + mView->columnData( mColIndex ).setWidth( mSize.getWidth(), true ); + mView->updateHeaderSize(); + mView->onColumnSizeChange( mColIndex, true ); + } else if ( mDragMode == DragMode::Reorder ) { + mView->mHeader->updateLayout(); + } + mDragMode = DragMode::None; return UIPushButton::onDragStop( pos, flags ); } diff --git a/src/eepp/ui/uitableview.cpp b/src/eepp/ui/uitableview.cpp index 2c55a8ad3..231cdbb7e 100644 --- a/src/eepp/ui/uitableview.cpp +++ b/src/eepp/ui/uitableview.cpp @@ -40,7 +40,7 @@ void UITableView::drawChildren() { (size_t)eeceil( ( mScrollOffset.y + mSize.getHeight() ) / rowHeight ), getItemCount() ); Float yOffset = 0; Float xOffset; - auto colCount = getModel()->columnCount(); + const auto& columnOrder = getColumnOrder(); auto headerHeight = getHeaderHeight(); for ( size_t i = start; i < end; i++ ) { xOffset = 0; @@ -53,7 +53,7 @@ void UITableView::drawChildren() { UITableRow* rowNode = updateRow( realRowIndex, rowIndex, yOffset ); rowNode->setChildrenVisibility( false, false ); realColIndex = 0; - for ( size_t colIndex = 0; colIndex < colCount; colIndex++ ) { + for ( size_t colIndex : columnOrder ) { auto& colData = columnData( colIndex ); if ( !colData.visible || ( xOffset + colData.width ) - mScrollOffset.x < 0 ) { if ( colData.visible ) diff --git a/src/eepp/ui/uitreeview.cpp b/src/eepp/ui/uitreeview.cpp index 2dc92427f..4d2e64d91 100644 --- a/src/eepp/ui/uitreeview.cpp +++ b/src/eepp/ui/uitreeview.cpp @@ -308,8 +308,7 @@ UIWidget* UITreeView::updateCell( const Vector2& posIndex, const ModelInd if ( hasChildren ) { UIIcon* icon = getIndexMetadata( index ).open ? mExpandIcon : mContractIcon; - DrawablePtr drawable = - icon ? icon->getSource( mExpanderIconSize ) : DrawablePtr{}; + DrawablePtr drawable = icon ? icon->getSource( mExpanderIconSize ) : DrawablePtr{}; if ( drawable == nullptr ) { image->setVisible( false ); @@ -396,7 +395,7 @@ void UITreeView::drawChildren() { UITableRow* rowNode = updateRow( v.realRowIndex, index, yOffset ); rowNode->setChildrenVisibility( false, false ); v.realColIndex = 0; - for ( size_t colIndex = 0; colIndex < getModel()->columnCount(); colIndex++ ) { + for ( size_t colIndex : getColumnOrder() ) { auto& colData = columnData( colIndex ); if ( !colData.visible || ( xOffset + colData.width ) - mScrollOffset.x < 0 ) { if ( colData.visible ) @@ -477,7 +476,10 @@ Node* UITreeView::overFind( const Vector2f& point ) { } bool UITreeView::isExpanded( const ModelIndex& index ) const { - return getIndexMetadata( index ).open; + if ( !index.isValid() ) + return false; + auto found = mViewMetadata.find( index.internalData() ); + return found != mViewMetadata.end() && found->second.open; } void UITreeView::setExpanded( const std::vector& indexes, bool expanded ) { diff --git a/src/tests/unit_tests/eproc_process_filter_tests.cpp b/src/tests/unit_tests/eproc_process_filter_tests.cpp new file mode 100644 index 000000000..a92b6bbff --- /dev/null +++ b/src/tests/unit_tests/eproc_process_filter_tests.cpp @@ -0,0 +1,43 @@ +#include "../../tools/eproc/process_model.hpp" +#include "utest.hpp" + +#include +#include +#include + +using namespace eproc; + +UTEST( EProcProcessFilter, ProgramsOnlyRequiresGuiWindow ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto model = ProcessModel::create( app.getUI() ); + UnorderedSet guiPids; + guiPids.insert( 3 ); + guiPids.insert( 4 ); + model->setGuiWindowPids( std::move( guiPids ) ); + model->setFilter( ProcessModel::ProgramsOnly ); + + ProcessInfo fish; + fish.pid = 1; + fish.name = "fish"; + fish.ttyNr = 123; + ProcessInfo ls; + ls.pid = 2; + ls.name = "ls"; + ls.ttyNr = 123; + ProcessInfo gui; + gui.pid = 3; + gui.name = "editor"; + gui.ttyNr = 123; + ProcessInfo tray; + tray.pid = 4; + tray.name = "tray app"; + model->applySnapshot( { fish, ls, gui, tray }, {} ); + + EXPECT_EQ( model->visibleCount(), 2u ); + EXPECT_EQ( model->rowForPid( 1 ), -1 ); + EXPECT_EQ( model->rowForPid( 2 ), -1 ); + EXPECT_TRUE( model->rowForPid( 3 ) >= 0 ); + EXPECT_TRUE( model->rowForPid( 4 ) >= 0 ); +} diff --git a/src/tests/unit_tests/eproc_process_tree_tests.cpp b/src/tests/unit_tests/eproc_process_tree_tests.cpp new file mode 100644 index 000000000..a3e7e64bc --- /dev/null +++ b/src/tests/unit_tests/eproc_process_tree_tests.cpp @@ -0,0 +1,77 @@ +#include "../../tools/eproc/process_model.hpp" +#include "utest.hpp" + +#include +#include +#include +#include + +using namespace eproc; + +namespace { + +ProcessInfo process( long pid, long parentPid, const char* name ) { + ProcessInfo info; + info.pid = pid; + info.parentPid = parentPid; + info.name = name; + return info; +} + +} // namespace + +UTEST( EProcProcessTree, ParentAndChildIndexesFollowPids ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto source = ProcessModel::create( app.getUI() ); + auto tree = ProcessTreeModel::create( source ); + source->setFilter( ProcessModel::AllProcessesInTreeForm ); + source->applySnapshot( { process( 1, 0, "init" ), process( 2, 1, "child" ), + process( 3, 2, "grandchild" ), process( 4, 999, "orphan" ) }, + {} ); + + EXPECT_EQ( tree->rowCount(), 2u ); + ModelIndex init = tree->indexForPid( 1 ); + ModelIndex child = tree->indexForPid( 2 ); + ModelIndex grandchild = tree->indexForPid( 3 ); + ModelIndex orphan = tree->indexForPid( 4 ); + ASSERT_TRUE( init.isValid() && child.isValid() && grandchild.isValid() && orphan.isValid() ); + EXPECT_EQ( tree->rowCount( init ), 1u ); + EXPECT_EQ( tree->rowCount( child ), 1u ); + EXPECT_TRUE( tree->parentIndex( child ) == init ); + EXPECT_TRUE( tree->parentIndex( grandchild ) == child ); + EXPECT_FALSE( tree->parentIndex( orphan ).isValid() ); + EXPECT_TRUE( tree->index( 0, ProcessModel::ColName, child ) == grandchild ); + EXPECT_EQ( tree->processForIndex( grandchild )->pid, 3 ); + + auto* view = UITreeView::New(); + view->setParent( app.getUI() ); + view->setPixelsSize( 500, 200 ); + view->setModel( tree ); + view->setExpanded( init, true ); + view->setExpanded( child, true ); + EXPECT_TRUE( view->isExpanded( init ) ); + EXPECT_TRUE( view->isExpanded( child ) ); +} + +UTEST( EProcProcessTree, SearchKeepsAncestorsAndOrphans ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto source = ProcessModel::create( app.getUI() ); + auto tree = ProcessTreeModel::create( source ); + source->setFilter( ProcessModel::AllProcessesInTreeForm ); + source->applySnapshot( { process( 1, 0, "init" ), process( 2, 1, "child" ), + process( 3, 2, "grandchild" ), process( 4, 999, "orphan" ) }, + {} ); + source->setTextFilter( "grandchild" ); + EXPECT_EQ( source->visibleCount(), 3u ); + EXPECT_EQ( source->textMatchedPids().size(), 1u ); + EXPECT_EQ( source->textMatchedPids().front(), 3 ); + EXPECT_EQ( tree->rowCount(), 1u ); + EXPECT_TRUE( tree->parentIndex( tree->indexForPid( 3 ) ) == tree->indexForPid( 2 ) ); + source->setTextFilter( "" ); + EXPECT_EQ( tree->rowCount(), 2u ); + EXPECT_TRUE( tree->indexForPid( 4 ).isValid() ); +} diff --git a/src/tests/unit_tests/eproc_window_icon_tests.cpp b/src/tests/unit_tests/eproc_window_icon_tests.cpp new file mode 100644 index 000000000..f3eb5556d --- /dev/null +++ b/src/tests/unit_tests/eproc_window_icon_tests.cpp @@ -0,0 +1,30 @@ +#include "../../tools/eproc/window_icon.hpp" +#include "utest.hpp" + +using namespace eproc; + +UTEST( EProcWindowIcon, ChoosesNearestUsableSizeAndConvertsArgbToRgba ) { + const unsigned long property[] = { + 1, 1, 0xFF112233, // 1x1 + 2, 2, 0x80445566, 0xFF000000, 0xFFFFFFFF, 0x00010203 // 2x2 + }; + WindowIconPixels icon = + decodeWindowIcon( property, sizeof( property ) / sizeof( property[0] ), 2 ); + ASSERT_TRUE( icon.valid() ); + EXPECT_EQ( icon.width, 2u ); + EXPECT_EQ( icon.height, 2u ); + EXPECT_EQ( icon.rgba.size(), 16u ); + EXPECT_EQ( icon.rgba[0], 0x44 ); + EXPECT_EQ( icon.rgba[1], 0x55 ); + EXPECT_EQ( icon.rgba[2], 0x66 ); + EXPECT_EQ( icon.rgba[3], 0x80 ); + EXPECT_EQ( icon.rgba[15], 0x00 ); +} + +UTEST( EProcWindowIcon, RejectsTruncatedOrInvalidProperty ) { + const unsigned long truncated[] = { 32, 32, 0xFFFFFFFF }; + EXPECT_FALSE( decodeWindowIcon( truncated, 3, 16 ).valid() ); + const unsigned long zeroWidth[] = { 0, 16 }; + EXPECT_FALSE( decodeWindowIcon( zeroWidth, 2, 16 ).valid() ); + EXPECT_FALSE( decodeWindowIcon( nullptr, 0, 16 ).valid() ); +} diff --git a/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp b/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp index 94dd31a70..e3c1b8fb6 100644 --- a/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp +++ b/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp @@ -2,11 +2,15 @@ #include "utest.hpp" #include #include +#include #include #include +#include #include #include #include +#include +#include #include using namespace EE; @@ -45,7 +49,8 @@ class ThreeColumnModel : public Model { } ModelIndex index( int row, int column, const ModelIndex& parent = ModelIndex() ) const override { - return row == 0 && column >= 0 && column < 3 && !parent.isValid() + return row >= 0 && static_cast( row ) < mRows && column >= 0 && column < 3 && + !parent.isValid() ? createIndex( row, column ) : ModelIndex{}; } @@ -57,6 +62,16 @@ class ThreeColumnModel : public Model { size_t mRows; }; +class SortableRowsModel : public ThreeColumnModel { + public: + SortableRowsModel() : ThreeColumnModel( 4 ) {} + + Variant data( const ModelIndex& index, ModelRole = ModelRole::Display ) const override { + static constexpr int values[] = { 30, 10, 40, 20 }; + return values[index.row()]; + } +}; + class PercentageTestTable : public UITableView { public: static PercentageTestTable* New() { return eeNew( PercentageTestTable, () ); } @@ -78,6 +93,14 @@ class PercentageTestTable : public UITableView { void updateScrollbars() { onContentSizeChange(); } + void dragColumnTo( size_t column, Float centerX ) { reorderColumnAt( column, centerX ); } + + void refreshColumns() { createOrUpdateColumns( true ); } + + void layoutHeaders() { mHeader->updateLayout(); } + + UIPushButton* header( size_t column ) { return columnData( column ).widget; } + Float getMaxColumnContentWidth( const size_t&, bool ) override { return mContentWidth; } protected: @@ -274,6 +297,169 @@ UTEST( UIAbstractTableView, ColumnWidthModeAndMenuCanBeConfiguredFromXML ) { EXPECT_TRUE( cssTable->isColumnWidthModeMenuEnabled() ); } +UTEST( UIAbstractTableView, ColumnOrderKeepsModelIndexesAndCellsAligned ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto model = std::make_shared(); + auto* table = UITableView::New(); + table->setParent( app.getUI() ); + table->setPixelsSize( 400, 200 ); + table->setModel( model ); + table->setColumnsWidth( 100 ); + + EXPECT_FALSE( table->isColumnReorderingEnabled() ); + table->setColumnReorderingEnabled( true ); + EXPECT_TRUE( table->isColumnReorderingEnabled() ); + EXPECT_FALSE( table->setColumnOrder( { 0, 0, 2 } ) ); + EXPECT_FALSE( table->setColumnOrder( { 0, 1 } ) ); + EXPECT_TRUE( table->setColumnOrder( { 2, 0, 1 } ) ); + EXPECT_TRUE( table->getColumnOrder() == std::vector( { 2, 0, 1 } ) ); + EXPECT_TRUE( table->getColumnPosition( 2 ).x < table->getColumnPosition( 0 ).x ); + EXPECT_TRUE( table->getColumnPosition( 0 ).x < table->getColumnPosition( 1 ).x ); + + table->drawChildren(); + for ( size_t column = 0; column < model->columnCount(); ++column ) { + auto* cell = table->getCellFromIndex( model->index( 0, column ) ); + ASSERT_TRUE( cell != nullptr ); + EXPECT_EQ( cell->getPixelsPosition().x, table->getColumnPosition( column ).x ); + } + + EXPECT_TRUE( table->moveColumn( 1, 0 ) ); + EXPECT_TRUE( table->getColumnOrder() == std::vector( { 1, 2, 0 } ) ); + table->setColumnHidden( 2, true ); + table->setColumnHidden( 2, false ); + EXPECT_TRUE( table->getColumnPosition( 1 ).x < table->getColumnPosition( 2 ).x ); + EXPECT_TRUE( table->getColumnPosition( 2 ).x < table->getColumnPosition( 0 ).x ); +} + +UTEST( UIAbstractTableView, DragOrderAndResizeUseVisualNeighbors ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto* table = PercentageTestTable::New(); + table->setParent( app.getUI() ); + table->setPixelsSize( 800, 200 ); + table->setModel( std::make_shared() ); + table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } ); + EXPECT_TRUE( table->setColumnOrder( { 2, 0, 1 } ) ); + + const Float contentWidth = table->getContentSpaceWidth(); + table->userResizeColumn( 2, contentWidth * 0.35f ); + EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 35.f ) < 0.5f ); + EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 15.f ) < 0.5f ); + EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 50.f ) < 0.5f ); + + const Float beyondLastColumn = table->getColumnPosition( 1 ).x + table->getColumnWidth( 1 ); + table->dragColumnTo( 2, beyondLastColumn ); + EXPECT_TRUE( table->getColumnOrder() == std::vector( { 0, 1, 2 } ) ); +} + +UTEST( UIAbstractTableView, RefreshKeepsDraggedHeaderPosition ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto* table = PercentageTestTable::New(); + table->setParent( app.getUI()->getRoot() ); + table->setPixelsSize( 500, 200 ); + table->setModel( std::make_shared() ); + table->setColumnsWidth( 100 ); + table->setColumnReorderingEnabled( true ); + app.getUI()->flushDirtyStyleAndLayout(); + app.getUI()->update( Milliseconds( 16 ) ); + auto* dragged = table->header( 0 ); + dragged->setDragging( true ); + dragged->setPixelsPosition( 47.f, dragged->getPixelsPosition().y ); + table->refreshColumns(); + EXPECT_EQ( dragged->getPixelsPosition().x, 47.f ); + table->layoutHeaders(); + EXPECT_EQ( dragged->getPixelsPosition().x, 47.f ); + dragged->setDragging( false, false ); +} + +UTEST( UIAbstractTableView, RightClickPreservesMultipleSelectedRows ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto model = std::make_shared( 3 ); + auto* table = UITableView::New(); + table->setParent( app.getUI() ); + table->setPixelsSize( 500, 200 ); + table->setModel( model ); + table->setSelectionKind( UIAbstractView::SelectionKind::Multiple ); + table->drawChildren(); + table->selectAll(); + EXPECT_EQ( table->getSelection().size(), 3 ); + EXPECT_TRUE( table->getSelection().containsRow( 0 ) ); + EXPECT_TRUE( table->getSelection().containsRow( 1 ) ); + EXPECT_TRUE( table->getSelection().containsRow( 2 ) ); + + const ModelIndex first = model->index( 0, 0 ); + const ModelIndex second = model->index( 1, 0 ); + const ModelIndex third = model->index( 2, 0 ); + table->getSelection().set( { first, second } ); + EXPECT_EQ( table->getSelection().size(), 2 ); + Input* input = app.getWindow()->getInput(); + InputEvent keyDown{}; + keyDown.Type = InputEvent::KeyDown; + keyDown.WinID = app.getWindow()->getWindowID(); + keyDown.key.keysym.mod = KeyMod::getDefaultModifier(); + input->processEventForWindow( &keyDown ); + InputEvent mouseDown{}; + mouseDown.Type = InputEvent::MouseButtonDown; + mouseDown.WinID = app.getWindow()->getWindowID(); + mouseDown.button.button = EE_BUTTON_RIGHT; + input->processEventForWindow( &mouseDown ); + + auto* secondRow = table->getCellFromIndex( second )->getParent(); + EXPECT_EQ( secondRow->asType()->getCurIndex().row(), 1 ); + secondRow->sendMouseEvent( Event::MouseDown, { 0, 0 }, EE_BUTTON_RMASK ); + EXPECT_EQ( table->getSelection().size(), 2 ); + EXPECT_TRUE( table->getSelection().contains( first ) ); + EXPECT_TRUE( table->getSelection().contains( second ) ); + + auto* thirdRow = table->getCellFromIndex( third )->getParent(); + EXPECT_EQ( thirdRow->asType()->getCurIndex().row(), 2 ); + thirdRow->sendMouseEvent( Event::MouseDown, { 0, 0 }, EE_BUTTON_RMASK ); + EXPECT_EQ( table->getSelection().size(), 1 ); + EXPECT_TRUE( table->getSelection().containsRow( third.row() ) ); +} + +UTEST( UIAbstractTableView, SortPreservesMultipleSelectedSourceRows ) { + UIApplication app( + WindowSettings( 600, 300, "eepp - unit tests" ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto source = std::make_shared(); + auto proxy = SortingProxyModel::New( source ); + auto* table = UITableView::New(); + table->setParent( app.getUI() ); + table->setPixelsSize( 500, 200 ); + table->setModel( proxy ); + table->setSelectionKind( UIAbstractView::SelectionKind::Multiple ); + table->getSelection().set( { proxy->index( 0, 2 ), proxy->index( 2, 1 ) } ); + auto* otherTable = UITableView::New(); + otherTable->setParent( app.getUI() ); + otherTable->setPixelsSize( 500, 200 ); + otherTable->setModel( proxy ); + otherTable->getSelection().set( proxy->index( 1, 0 ) ); + + auto selectedSourceIndexes = [&]() { + std::vector> indexes; + for ( const auto& index : table->getSelection().indexes() ) + indexes.emplace_back( proxy->mapToSource( index ).row(), index.column() ); + std::sort( indexes.begin(), indexes.end() ); + return indexes; + }; + const std::vector> expected{ { 0, 2 }, { 2, 1 } }; + EXPECT_TRUE( selectedSourceIndexes() == expected ); + table->sortByColumn( 0, SortOrder::Ascending ); + EXPECT_TRUE( selectedSourceIndexes() == expected ); + EXPECT_EQ( proxy->mapToSource( otherTable->getSelection().first() ).row(), 1 ); + table->sortByColumn( 0, SortOrder::Descending ); + EXPECT_TRUE( selectedSourceIndexes() == expected ); + EXPECT_EQ( proxy->mapToSource( otherTable->getSelection().first() ).row(), 1 ); +} + UTEST( UIAbstractTableView, PercentageRoundingDoesNotCreatePhantomHorizontalScroll ) { Float previousDensity = PixelDensity::getPixelDensity(); PixelDensity::setPixelDensity( 1.5f ); diff --git a/src/tools/eeiv/eeiv.cpp b/src/tools/eeiv/eeiv.cpp index dd959796d..34d27a468 100644 --- a/src/tools/eeiv/eeiv.cpp +++ b/src/tools/eeiv/eeiv.cpp @@ -184,7 +184,7 @@ bool App::init() { contextSettings.FrameRateLimit = mConfig.FrameLimit; contextSettings.SharedGLContext = true; windowSettings.Title = "eeiv"; - windowSettings.Icon = Sys::getProcessPath() + "assets/eeiv.png"; + windowSettings.Icon = Sys::getProcessPath() + "assets/icon/eeiv.png"; if ( !FileSystem::fileExists( windowSettings.Icon ) ) windowSettings.Icon = Sys::getProcessPath() + "assets/icon/ee.png"; diff --git a/src/tools/eproc/appconfig.cpp b/src/tools/eproc/appconfig.cpp index f30130e9e..f05b5a2f5 100644 --- a/src/tools/eproc/appconfig.cpp +++ b/src/tools/eproc/appconfig.cpp @@ -24,6 +24,11 @@ void AppConfig::load() { mState.getValueI( "window", "display_index", windowState.displayIndex ); windowState.maximized = mState.getValueB( "window", "maximized", windowState.maximized ); processTableState = mState.getValue( "process_table", "state", processTableState ); + divideCpuUsage = + mState.getValueB( "process_table", "divide_cpu_usage", + mState.getValueB( "window", "divide_cpu_usage", divideCpuUsage ) ); + treeView = mState.getValueB( "process_table", "tree_view", treeView ); + filterMode = mState.getValueI( "process_table", "filter_mode", treeView ? 1 : filterMode ); } bool AppConfig::saveWindowState() { @@ -37,6 +42,9 @@ bool AppConfig::saveWindowState() { mState.setValueI( "window", "display_index", windowState.displayIndex ); mState.setValueB( "window", "maximized", windowState.maximized ); mState.setValue( "process_table", "state", processTableState ); + mState.setValueB( "process_table", "divide_cpu_usage", divideCpuUsage ); + mState.setValueB( "process_table", "tree_view", treeView ); + mState.setValueI( "process_table", "filter_mode", filterMode ); return mState.writeFile(); } diff --git a/src/tools/eproc/appconfig.hpp b/src/tools/eproc/appconfig.hpp index 246d0c2e7..57a9b8e65 100644 --- a/src/tools/eproc/appconfig.hpp +++ b/src/tools/eproc/appconfig.hpp @@ -35,6 +35,9 @@ class AppConfig { WindowStateConfig windowState; // Serialized process table columns, widths, and sorting state. std::string processTableState; + bool divideCpuUsage{ false }; + bool treeView{ false }; + int filterMode{ 0 }; private: std::string mConfigPath; diff --git a/src/tools/eproc/eproc.cpp b/src/tools/eproc/eproc.cpp index 9da49458b..ea4ee6e64 100644 --- a/src/tools/eproc/eproc.cpp +++ b/src/tools/eproc/eproc.cpp @@ -1,5 +1,6 @@ #include "eproc.hpp" +#include #include #include #include @@ -7,6 +8,7 @@ #include #include +#include #include #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_BSD @@ -20,11 +22,13 @@ namespace eproc { namespace { -constexpr int kProcessTableStateVersion = 3; -constexpr int kPreviousProcessTableStateVersion = 2; +constexpr int kProcessTableStateVersion = 4; +constexpr int kPreviousProcessTableStateVersion = 3; +constexpr int kLegacyProcessTableStateVersion = 2; +constexpr size_t kPreviousProcessColumnCount = 20; constexpr size_t kPreviousCommandColumn = 11; -constexpr std::array kOptionalProcessColumns = { { +constexpr std::array kOptionalProcessColumns = { { ProcessModel::ColTotalMemory, ProcessModel::ColVirtualSize, ProcessModel::ColCpuTime, @@ -33,6 +37,8 @@ constexpr std::array kOptionalProcessColumns = { { ProcessModel::ColTty, ProcessModel::ColIoRead, ProcessModel::ColIoWrite, + ProcessModel::ColThreads, + ProcessModel::ColMemoryPercent, } }; const char* sortOrderName( SortOrder order ) { @@ -95,13 +101,14 @@ size_t remapPreviousProcessTableColumn( size_t column ) { bool migratePreviousProcessTableState( nlohmann::json& state ) { if ( !state.contains( "widths" ) || !state["widths"].is_object() || !state["widths"].contains( "widths" ) || !state["widths"]["widths"].is_array() || - state["widths"]["widths"].size() != ProcessModel::ColCount ) + state["widths"]["widths"].size() != kPreviousProcessColumnCount ) return false; nlohmann::json remappedWidths = nlohmann::json::array(); - for ( size_t column = 0; column < ProcessModel::ColCount; ++column ) + for ( size_t column = 0; column < kPreviousProcessColumnCount; ++column ) remappedWidths.push_back( 0 ); - for ( size_t previousColumn = 0; previousColumn < ProcessModel::ColCount; ++previousColumn ) { + for ( size_t previousColumn = 0; previousColumn < kPreviousProcessColumnCount; + ++previousColumn ) { remappedWidths[remapPreviousProcessTableColumn( previousColumn )] = state["widths"]["widths"][previousColumn]; } @@ -114,7 +121,7 @@ bool migratePreviousProcessTableState( nlohmann::json& state ) { continue; const Int64 previousColumn = column.get(); if ( previousColumn >= 0 && - static_cast( previousColumn ) < ProcessModel::ColCount ) + static_cast( previousColumn ) < kPreviousProcessColumnCount ) remappedHiddenColumns.push_back( remapPreviousProcessTableColumn( static_cast( previousColumn ) ) ); } @@ -124,11 +131,31 @@ bool migratePreviousProcessTableState( nlohmann::json& state ) { if ( state.contains( "sort" ) && state["sort"].is_object() && state["sort"].contains( "column" ) && state["sort"]["column"].is_number_integer() ) { const Int64 previousColumn = state["sort"]["column"].get(); - if ( previousColumn >= 0 && static_cast( previousColumn ) < ProcessModel::ColCount ) + if ( previousColumn >= 0 && + static_cast( previousColumn ) < kPreviousProcessColumnCount ) state["sort"]["column"] = remapPreviousProcessTableColumn( static_cast( previousColumn ) ); } + state["version"] = kPreviousProcessTableStateVersion; + return true; +} + +bool migrateProcessTableState( nlohmann::json& state ) { + if ( state["version"] == kLegacyProcessTableStateVersion && + !migratePreviousProcessTableState( state ) ) + return false; + if ( state["version"] != kPreviousProcessTableStateVersion || !state.contains( "widths" ) || + !state["widths"].is_object() || !state["widths"].contains( "widths" ) || + !state["widths"]["widths"].is_array() || + state["widths"]["widths"].size() != kPreviousProcessColumnCount ) + return false; + state["widths"]["widths"].push_back( 0 ); + state["widths"]["widths"].push_back( 0 ); + if ( !state.contains( "hidden_columns" ) || !state["hidden_columns"].is_array() ) + state["hidden_columns"] = nlohmann::json::array(); + state["hidden_columns"].push_back( ProcessModel::ColThreads ); + state["hidden_columns"].push_back( ProcessModel::ColMemoryPercent ); state["version"] = kProcessTableStateVersion; return true; } @@ -169,36 +196,46 @@ constexpr std::array kUsernameClasses = { "eproc-process-username-system", "eproc-process-username-other", }; +constexpr std::array kCpuFillClasses = { + "eproc-cpu-fill-0", "eproc-cpu-fill-10", "eproc-cpu-fill-20", "eproc-cpu-fill-30", + "eproc-cpu-fill-40", "eproc-cpu-fill-50", "eproc-cpu-fill-60", "eproc-cpu-fill-70", + "eproc-cpu-fill-80", "eproc-cpu-fill-90", "eproc-cpu-fill-100", +}; + } // namespace App::App() { mConfig = std::make_unique( Sys::getConfigPath( "eproc" ) ); mConfig->load(); - - // Scanning /proc takes long enough to race the first frame, which would leave the table briefly - // empty. Kick the worker off before the window exists so the first snapshot is already staged - // by the time the scene is rendered. - startCollection(); - - const Sizei storedSize = mConfig->windowState.size; - const Uint32 windowWidth = - storedSize.getWidth() > 0 ? static_cast( storedSize.getWidth() ) : 1280; - const Uint32 windowHeight = - storedSize.getHeight() > 0 ? static_cast( storedSize.getHeight() ) : 720; - WindowSettings ws( windowWidth, windowHeight, "", WindowStyle::Default, WindowBackend::Default, - 32, Sys::getProcessPath() + "assets/icon/ee.png" ); - ContextSettings ctx; - ctx.Multisamples = 4; - mApp = std::make_unique( ws, UIApplication::Settings{}, ctx ); - if ( mApp->getUI() && mApp->getWindow() ) { - mApp->getWindow()->setTitle( - mApp->getUI()->i18n( "eproc_window_title", "eproc - System Monitor" ) ); - } } App::~App() {} -int App::run() { +int App::run( int argc, char* argv[] ) { + args::ArgumentParser parser( "eproc" ); + args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } ); + args::ValueFlag pixelDensity( parser, "pixel-density", + "Set default application pixel density", + { 'd', "pixel-density" } ); + try { + parser.ParseCLI( argc, argv ); + } catch ( const args::Help& ) { + std::cout << parser; + return EXIT_SUCCESS; + } catch ( const args::ParseError& error ) { + std::cerr << error.what() << '\n' << parser; + return EXIT_FAILURE; + } catch ( const args::ValidationError& error ) { + std::cerr << error.what() << '\n' << parser; + return EXIT_FAILURE; + } + if ( pixelDensity ) { + if ( !std::isfinite( pixelDensity.Get() ) || pixelDensity.Get() <= 0 ) { + std::cerr << "Pixel density must be a positive finite number\n"; + return EXIT_FAILURE; + } + mPixelDensity = pixelDensity.Get(); + } if ( !init() ) return EXIT_FAILURE; const int result = mApp->run(); @@ -208,6 +245,25 @@ int App::run() { } bool App::init() { + // Start collection before creating the window so the first frame can use the staged snapshot. + startCollection(); + const Sizei storedSize = mConfig->windowState.size; + const Uint32 windowWidth = + storedSize.getWidth() > 0 ? static_cast( storedSize.getWidth() ) : 1280; + const Uint32 windowHeight = + storedSize.getHeight() > 0 ? static_cast( storedSize.getHeight() ) : 720; + WindowSettings ws( windowWidth, windowHeight, "", WindowStyle::Default, WindowBackend::Default, + 32, Sys::getProcessPath() + "assets/icon/eproc.png" ); + ContextSettings ctx; + ctx.Multisamples = 4; + UIApplication::Settings settings; + settings.pixelDensity = mPixelDensity; + mApp = std::make_unique( ws, settings, ctx ); + if ( mApp->getUI() && mApp->getWindow() ) { + mApp->getWindow()->setTitle( + mApp->getUI()->i18n( "eproc_window_title", "eproc - System Monitor" ) ); + } + auto* ui = mApp->getUI(); if ( !ui ) return false; @@ -224,50 +280,126 @@ bool App::init() { mRoot = ui->loadLayoutFromString( R"xml(