Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Every change is written to the active profile in settings.ini and applies at onc
| F2, Delete, Shift+Delete, Ctrl+C/X/V, Ctrl+A, Alt+Enter | Shell verbs on the selection (F2 on shell items only) |
| Right-click | Full shell context menu (Shift adds the extended verbs); the background adds View, Sort by, Refresh, Paste, Pin on top; archives get the extract menu |
| Ctrl+Shift+E | Open the current folder in Windows Explorer (an archive: its folder) |
| Ctrl+T, Ctrl+W | New tab at the same place, close tab (closing the last tab closes the window) |
| Ctrl+T, Ctrl+W | New tab at the start place, close tab (closing the last tab closes the window) |
| Ctrl+Tab, Ctrl+Shift+Tab, Ctrl+1..8, Ctrl+9 | Next, previous, nth, last tab |
| Middle-click a folder | Open it in a background tab (middle-click a tab closes it) |
| Ctrl+P | Palette: recent places, bookmarks, cloud roots and the current subfolders, fuzzy matched |
Expand Down Expand Up @@ -243,8 +243,9 @@ else Explorer-style (`/idlist,...`, an unknown `/` switch, URLs such as `ms-sett
Paths) and the process exits. A second launch hands its folders (and `--search`) to the running window and
exits (36-78 ms measured from process creation to the new tab's title); a second launch without folders (Win+E once
installed) opens a tab at the profile's start place. One instance runs per settings folder, so a launch with
another `%APPDATA%` (the smoke test) runs on its own. A plain first launch restores the session; a launch with
folders opens just those. After a crash the process relaunches itself with `--recover`; each `--recover` start
another `%APPDATA%` (the smoke test) runs on its own. A plain first launch opens a tab at the start place, as
Explorer does (view and panes still come from the session); a launch with folders opens just those. After a crash
the process relaunches itself with `--recover`; each `--recover` start
counts itself in `%APPDATA%\rexplorer\crash-count.txt`, and the second within 60 s opens the folder in Windows
Explorer instead of running (so a crash loop stops at Explorer). A start without `--recover` resets the count.
Ctrl+Shift+E and the fallbacks start `%SystemRoot%\explorer.exe` by full path, so they reach Explorer even when
Expand Down Expand Up @@ -343,7 +344,7 @@ Home does not exist (older Windows), the default start is This PC. Lines startin
profile=default ; the active profile

[profile default]
start=shell:::{F874310E-B6B7-47DC-BC84-B9E6B38F5903} ; where a first run (or a launch without a session) starts: Home
start=shell:::{F874310E-B6B7-47DC-BC84-B9E6B38F5903} ; where a launch without folders, and a new tab, start: Home
theme=dark ; a file name in themes\ without .ini
view=details ; details | icons | thumbnails (the view segment writes it back here)
show_tree=false ; the folder tree instead of the sidebar
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub(crate) unsafe fn update(app: *mut App) {
(CMD_PASTE, shell && clipboard_has_files()),
(CMD_RENAME, shell && sel == 1),
(CMD_SHARE, shell && sel > 0),
(CMD_DELETE, (shell || search) && sel > 0),
(CMD_DELETE, (shell || search) && sel > 0 && list::can_delete(app, &list::selected_indices(a.list))),
(CMD_SORT, shell && !home),
] {
toolbar::set_enabled(bar, cmd, on);
Expand Down
63 changes: 62 additions & 1 deletion crates/app/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,28 @@ pub(crate) unsafe fn selection_menu(app: *mut App, rows: &[usize]) -> Option<Con
}
}

/// False when a selected shell item cannot be deleted, such as Home's OneDrive cloud recent files: the shell's
/// delete verb fails on them with 0x80004002. Home reports no SFGAO_CANDELETE even for its local files, which do
/// delete, so a file system item counts as deletable. A file system folder (its parsing name is a path) is not
/// checked: Ctrl+A in a huge folder would ask every item.
pub(crate) unsafe fn can_delete(app: *mut App, rows: &[usize]) -> bool {
use windows::Win32::System::SystemServices::{SFGAO_CANDELETE, SFGAO_FILESYSTEM};
let Some(Source::Shell(l)) = source(app) else { return true };
if std::path::Path::new(&l.folder.parsing_name).is_absolute() {
return true;
}
let mask = SFGAO_CANDELETE.0 | SFGAO_FILESYSTEM.0;
rows.iter().filter_map(|&i| l.items.get(i)).all(|it| it.attributes(mask) & mask != 0)
}

pub(crate) unsafe fn verb_on_selection(app: *mut App, verb: &str, shift: bool) {
let hwnd = (*app).hwnd;
if let Some(menu) = selection_menu(app, &selected_indices((*app).list)) {
let rows = selected_indices((*app).list);
if verb == "delete" && !can_delete(app, &rows) {
let _ = MessageBeep(MB_OK);
return;
}
if let Some(menu) = selection_menu(app, &rows) {
let _ = menu.invoke_verb(hwnd, verb, shift);
}
}
Expand Down Expand Up @@ -538,6 +557,9 @@ unsafe fn draw_item(app: *mut App, cd: &mut NMLVCUSTOMDRAW) -> u32 {
}
}

/// The list's own timer that restores full-row select during a marquee (an id the ListView's own timers do not use).
const MARQUEE_TIMER: usize = 0x5258;

/// The header is 32 px high (96 DPI) whatever the font asks for.
pub(crate) unsafe extern "system" fn header_subclass(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, _id: usize, _data: usize) -> LRESULT {
let r = DefSubclassProc(hwnd, msg, wparam, lparam);
Expand Down Expand Up @@ -574,6 +596,45 @@ pub(crate) unsafe extern "system" fn subclass(hwnd: HWND, msg: u32, wparam: WPAR
show_menu(app, on_item, Some(menu_point(hwnd, on_item.then_some(focus as usize))));
return LRESULT(0);
}
// Explorer box-selects when a drag starts on a row's other columns; a full-row ListView would drag the item. So
// such a press reaches the ListView with full-row select off: to it the press is on blank space and a drag draws
// a marquee. A timer that fires inside the marquee's loop turns full-row back on, else the marquee would only
// take rows whose name it touches. A press released without a drag runs again with full-row on, as a click on
// the row. shortcut: a drag from blank space right of a name drags the item, unlike Explorer; measure the name's
// text width if that matters.
let full = LVS_EX_FULLROWSELECT as usize;
if msg == WM_TIMER && wparam.0 == MARQUEE_TIMER {
let _ = KillTimer(Some(hwnd), MARQUEE_TIMER);
SendMessageW(hwnd, LVM_SETEXTENDEDLISTVIEWSTYLE, Some(WPARAM(full)), Some(LPARAM(full as isize)));
return LRESULT(0);
}
if msg == WM_LBUTTONDOWN && (*app).views.view == views::View::Details {
let pt = POINT { x: lparam.0 as i16 as i32, y: (lparam.0 >> 16) as i16 as i32 };
let mut ht = LVHITTESTINFO { pt, ..Default::default() };
SendMessageW(hwnd, LVM_SUBITEMHITTEST, None, Some(LPARAM(&mut ht as *mut _ as isize)));
if ht.iItem >= 0 && ht.iSubItem > 0 {
SendMessageW(hwnd, LVM_SETEXTENDEDLISTVIEWSTYLE, Some(WPARAM(full)), Some(LPARAM(0)));
SetTimer(Some(hwnd), MARQUEE_TIMER, 1, None);
let r = DefSubclassProc(hwnd, msg, wparam, lparam);
let _ = KillTimer(Some(hwnd), MARQUEE_TIMER);
SendMessageW(hwnd, LVM_SETEXTENDEDLISTVIEWSTYLE, Some(WPARAM(full)), Some(LPARAM(full as isize)));
// Where the button came up: the last message the ListView's mouse loop took.
let up = GetMessagePos();
let mut down = pt;
let _ = ClientToScreen(hwnd, &mut down);
let dragged = (up as i16 as i32 - down.x).abs() > GetSystemMetrics(SM_CXDRAG)
|| ((up >> 16) as i16 as i32 - down.y).abs() > GetSystemMetrics(SM_CYDRAG);
if dragged {
return r;
}
// The ListView's click handling waits for a button-up; the first pass took the real one.
let mut queued = MSG::default();
if !PeekMessageW(&mut queued, Some(hwnd), WM_LBUTTONUP, WM_LBUTTONUP, PM_NOREMOVE).as_bool() {
let _ = PostMessageW(Some(hwnd), WM_LBUTTONUP, WPARAM(wparam.0 & !(windows::Win32::System::SystemServices::MK_LBUTTON.0 as usize)), lparam);
}
return DefSubclassProc(hwnd, msg, wparam, lparam);
}
}
if msg == WM_MOUSEMOVE {
if !(*app).panes.hover_tracking {
let mut tme = TRACKMOUSEEVENT { cbSize: size_of::<TRACKMOUSEEVENT>() as u32, dwFlags: TME_LEAVE, hwndTrack: hwnd, dwHoverTime: 0 };
Expand Down
10 changes: 5 additions & 5 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,11 +436,11 @@ fn accels(overrides: &[(String, String)]) -> Vec<ACCEL> {
keys::apply(&ACCELS, overrides).into_iter().chain(digits.iter().enumerate().map(|(i, &k)| accel(FCONTROL.0, k, CMD_TAB1 + i as u16))).collect()
}

/// The session's tabs on a plain launch or --recover; otherwise the command-line folders; else the profile's start.
/// The session's tabs on --recover (as Explorer, a plain launch does not restore them); otherwise the command-line
/// folders; else the profile's start.
unsafe fn open_startup_tabs(app: *mut App, args: &cli::Args) {
let plain = args.paths.is_empty() && args.select.is_none();
let saved = std::mem::take(&mut (*app).session.tabs);
if (args.recover || plain) && !saved.is_empty() {
if args.recover && !saved.is_empty() {
let active = (*app).session.active;
for name in saved {
nav::new_tab(app, nav::Target::Name(name), false, None);
Expand Down Expand Up @@ -629,7 +629,7 @@ unsafe fn on_notify(app: *mut App, lparam: LPARAM) -> LRESULT {
match hdr.code {
tabs::TN_SELECT => nav::switch_tab(app, nm.index),
tabs::TN_CLOSE => nav::close_tab(app, nm.index),
tabs::TN_NEW => nav::duplicate_tab(app),
tabs::TN_NEW => nav::new_start_tab(app),
_ => {}
}
} else if from == (*app).crumbs {
Expand Down Expand Up @@ -717,7 +717,7 @@ unsafe fn on_command(app: *mut App, cmd: u16) {
list::verb_on_selection(app, "properties", false);
}
}
CMD_NEW_TAB => nav::duplicate_tab(app),
CMD_NEW_TAB => nav::new_start_tab(app),
CMD_CLOSE_TAB => nav::close_tab(app, (*app).active),
CMD_NEXT_TAB => nav::cycle_tab(app, 1),
CMD_PREV_TAB => nav::cycle_tab(app, -1),
Expand Down
10 changes: 3 additions & 7 deletions crates/app/src/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,13 +624,9 @@ pub(crate) unsafe fn new_tab(app: *mut App, target: Target, activate: bool, sele
session_changed(app);
}

/// A new tab at the active tab's place (Ctrl+T, the strip's + button); a search tab duplicates its folder.
pub(crate) unsafe fn duplicate_tab(app: *mut App) {
let target = match source(app) {
Some(s) => s.location(),
None => Target::Name((*app).profile.start.clone()),
};
new_tab(app, target, true, None);
/// Ctrl+T and the tab strip's +: a tab at the profile's start place, as in Explorer.
pub(crate) unsafe fn new_start_tab(app: *mut App) {
new_tab(app, Target::Name((*app).profile.start.clone()), true, None);
}

pub(crate) unsafe fn switch_tab(app: *mut App, t: usize) {
Expand Down
Loading