diff --git a/source/interface_derived.cpp b/source/interface_derived.cpp index c63a3aec..0e204333 100644 --- a/source/interface_derived.cpp +++ b/source/interface_derived.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include +#include #include #if __APPLE__ @@ -30,6 +32,14 @@ const int TIMER_LENGTH = 5 * 1000 * 60; constexpr std::string_view config_name = "com.ravbug.unityhubnative.config"; constexpr std::string_view config_sortCol = "sortColIndex"; constexpr std::string_view config_sortAscending = "sortAscending"; +constexpr std::string_view config_windowWidth = "windowWidth"; +constexpr std::string_view config_windowHeight = "windowHeight"; +constexpr std::string_view config_windowX = "windowX"; +constexpr std::string_view config_windowY = "windowY"; +constexpr std::string_view config_columnWidth = "projectColumnWidth"; + +// menu command ids that have no fitting stock id +constexpr int ID_CENTER_ON_DISPLAY = wxID_HIGHEST + 1; //Declare events here wxBEGIN_EVENT_TABLE(MainFrameDerived, wxFrame) @@ -43,6 +53,10 @@ EVT_MENU(wxID_REFRESH, MainFrameDerived::OnReloadData) EVT_MENU(wxID_UNINDENT, MainFrameDerived::OnSource) EVT_MENU(wxID_TOP, MainFrameDerived::OnUpdate) EVT_MENU(wxID_EXIT, MainFrameDerived::OnQuit) +EVT_MENU(ID_CENTER_ON_DISPLAY, MainFrameDerived::OnCenterOnDisplay) +EVT_CLOSE(MainFrameDerived::OnClose) +EVT_SIZE(MainFrameDerived::OnSize) +EVT_MOVE(MainFrameDerived::OnMove) EVT_BUTTON(wxID_ADD,MainFrameDerived::OnAddProject) EVT_BUTTON(wxID_NEW,MainFrameDerived::OnCreateProject) EVT_BUTTON(wxID_FIND,MainFrameDerived::OnLocateInstall) @@ -61,6 +75,7 @@ EVT_LIST_ITEM_SELECTED(wxID_HARDDISK, MainFrameDerived::OnSelectProject) EVT_LIST_ITEM_DESELECTED(wxID_HARDDISK, MainFrameDerived::OnDeselectProject) EVT_LIST_DELETE_ITEM(wxID_HARDDISK, MainFrameDerived::OnDeselectProject) EVT_LIST_COL_CLICK(wxID_HARDDISK, MainFrameDerived::OnColumnClick) +EVT_LIST_COL_END_DRAG(wxID_HARDDISK, MainFrameDerived::OnColumnResized) EVT_LISTBOX(wxID_FLOPPY, MainFrameDerived::OnSelectEditor) EVT_LISTBOX(wxID_HOME, MainFrameDerived::OnSelectEditorPath) @@ -74,11 +89,11 @@ wxEND_EVENT_TABLE() //call superclass constructor MainFrameDerived::MainFrameDerived() : MainFrame(NULL){ - // load prefs wxConfig config(config_name.data()); + sortColumn = std::clamp(config.ReadLong(config_sortCol.data(), 0), 0, 3); - sortAscending = config.ReadBool(config_sortAscending.data(), false); + sortAscending = config.ReadBool(config_sortAscending.data(), true); //set up project list columns { @@ -87,6 +102,22 @@ MainFrameDerived::MainFrameDerived() : MainFrame(NULL){ projectsList->AppendColumn(str,wxLIST_FORMAT_LEFT); } } + //restore column widths if the user resized them in a previous session + { + const int cols = projectsList->GetColumnCount(); + std::vector widths(cols); + bool allRead = true; + for (int i = 0; i < cols && allRead; i++){ + allRead = config.Read(std::format("{}{}", config_columnWidth, i), &widths[i]); + } + if (allRead){ + for (int i = 0; i < cols; i++){ + projectsList->SetColumnWidth(i, widths[i]); + } + customColumnWidths = true; + } + } + projectsList->ShowSortIndicator(sortColumn, sortAscending); //make the data folder if it does not already exist (with readwrite for all groups) #if defined __APPLE__ || defined __linux__ int status = mkdir(datapath.c_str(),S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); @@ -102,6 +133,26 @@ MainFrameDerived::MainFrameDerived() : MainFrame(NULL){ //set reveal label menuReveal->SetItemLabel("Reveal In File Explorer\tCtrl-F"); #endif + + // recovery command for a window that ended up off screen (e.g. its saved display was disconnected). + // Added here rather than in the generated base class so regenerating interface.cpp does not drop it. + // The shortcut matters on Windows and Linux: the menu bar is part of the window there, so when the + // window is off screen the menu is unreachable, but the shortcut still works once the window is focused + if (const int windowMenuIndex = menubar->FindMenu("Window"); windowMenuIndex != wxNOT_FOUND) { + menubar->GetMenu(windowMenuIndex)->Insert(1, ID_CENTER_ON_DISPLAY, "Center Window on Current Display\tCtrl-Shift-M"); + } + + // set saved position and size. This must come after dpi_scale(), which calls Fit() on Windows + // and would otherwise shrink the window back to its default size + int width, height, x, y; + if (config.Read(config_windowWidth.data(), &width) && config.Read(config_windowHeight.data(), &height) && + config.Read(config_windowX.data(), &x) && config.Read(config_windowY.data(), &y)) { + this->SetSize(x, y, width, height); + // the saved geometry may no longer fit the displays, e.g. its monitor was disconnected or the layout changed + KeepOnScreen(); + } + windowRect = this->GetRect(); + if (status != 0){ ReloadData(); } @@ -114,6 +165,139 @@ MainFrameDerived::MainFrameDerived() : MainFrame(NULL){ projSearchCtrl->SetFocus(); } +void MainFrameDerived::OnQuit(wxCommandEvent&){ + Close(); +} + +void MainFrameDerived::OnClose(wxCloseEvent& event){ + // save the last window size and position from before it was minimized (if it is) + wxConfig config(config_name.data()); + config.Write(config_windowWidth.data(), windowRect.width); + config.Write(config_windowHeight.data(), windowRect.height); + config.Write(config_windowX.data(), windowRect.x); + config.Write(config_windowY.data(), windowRect.y); + + // save column widths only if the user resized them, otherwise keep autosizing + for (int i = 0; i < projectsList->GetColumnCount(); i++){ + const auto key = std::format("{}{}", config_columnWidth, i); + if (customColumnWidths){ + config.Write(key, projectsList->GetColumnWidth(i)); + } + else{ + config.DeleteEntry(key); + } + } + + // let the default handler destroy the window + event.Skip(); +} + +void MainFrameDerived::OnSize(wxSizeEvent& event){ + RememberWindowRect(); + event.Skip(); +} + +void MainFrameDerived::OnMove(wxMoveEvent& event){ + RememberWindowRect(); + event.Skip(); +} + +/** + Shrinks the rect to fit into the area if it is larger, then moves it the shortest distance that puts it fully inside + */ +static wxRect FitInto(wxRect rect, const wxRect& area){ + rect.width = std::min(rect.width, area.width); + rect.height = std::min(rect.height, area.height); + rect.x = std::clamp(rect.x, area.x, area.x + area.width - rect.width); + rect.y = std::clamp(rect.y, area.y, area.y + area.height - rect.height); + return rect; +} + +static long long AreaOf(const wxRect& rect){ + return rect.IsEmpty() ? 0 : static_cast(rect.width) * rect.height; +} + +/** + Moves the window to the center of the display the user is working on (the one under the mouse pointer), + shrinking it to fit if needed. Recovers a window that is off screen or on a display that is gone + */ +void MainFrameDerived::OnCenterOnDisplay(wxCommandEvent&){ + int displayIndex = wxDisplay::GetFromPoint(wxGetMousePosition()); + if (displayIndex == wxNOT_FOUND) { + displayIndex = 0; // the primary display + } + // the client area excludes the taskbar, the Dock and the macOS menu bar + const wxRect area = wxDisplay(static_cast(displayIndex)).GetClientArea(); + + // leave the states whose geometry is managed by the OS, so the window can be moved + if (IsFullScreen()) { + ShowFullScreen(false); + } + if (IsIconized()) { + Iconize(false); + } + if (IsMaximized()) { + Maximize(false); + } + + SetSize(FitInto(GetRect(), area).CenterIn(area)); + Raise(); +} + +/** + If part of the window is outside every display, snaps it back to the nearest edge of the display it overlaps the most + (or the closest display, if it overlaps none), shrinking it if it does not fit there. + A window spanning several displays is left alone as long as all of it is visible + */ +void MainFrameDerived::KeepOnScreen(){ + const unsigned displayCount = wxDisplay::GetCount(); + if (displayCount == 0) { + return; + } + const wxRect rect = GetRect(); + + // The client areas exclude the taskbar, the Dock and the macOS menu bar, and do not overlap each other, so the + // window is fully visible exactly when their overlaps with it add up to its whole area. The visibility test + // ignores a margin around the window: on Windows 10/11 GetRect() includes the invisible resize borders, so a + // window snapped to a screen edge sticks out by a few pixels and must not be treated as off screen + // (const matters: the non-const wxRect::Intersect() overload intersects in place instead of returning a copy) + const wxRect deflated = wxRect(rect).Deflate(FromDIP(16)); + const wxRect tested = deflated.IsEmpty() ? rect : deflated; + + long long visibleArea = 0; + long long bestOverlap = 0; + long long bestDistance = std::numeric_limits::max(); + wxRect target; + for (unsigned i = 0; i < displayCount; i++){ + const wxRect area = wxDisplay(i).GetClientArea(); + visibleArea += AreaOf(tested.Intersect(area)); + + // prefer the display holding most of the window; failing that, the one with the closest edge + const long long overlap = AreaOf(rect.Intersect(area)); + const long long dx = std::max({0, area.x - rect.GetRight(), rect.x - area.GetRight()}); + const long long dy = std::max({0, area.y - rect.GetBottom(), rect.y - area.GetBottom()}); + const long long distance = dx * dx + dy * dy; + if (overlap > bestOverlap || (bestOverlap == 0 && overlap == 0 && distance < bestDistance)) { + bestOverlap = overlap; + bestDistance = distance; + target = area; + } + } + + if (visibleArea < AreaOf(tested)) { + SetSize(FitInto(rect, target)); + } +} + +/** + Stores the current window geometry, unless the window is minimized (its geometry is not meaningful then) + */ +void MainFrameDerived::RememberWindowRect(){ + if (!this->IsIconized()) { + windowRect = this->GetRect(); + } +} + void MainFrameDerived::OnSelectProject(wxListEvent&){ for (auto ptr : projectActionItems){ ptr->Enable(); @@ -158,11 +342,6 @@ void MainFrameDerived::OnSelectEditorPath(wxCommandEvent&){ } } -void MainFrameDerived::OnQuit(wxCommandEvent&) -{ - Close(); -} - /** Loads the data in the main view. If anything is currently loaded, it will be cleared and re-loaded */ @@ -607,10 +786,12 @@ void MainFrameDerived::AddProject(const project& p, const std::string& filter, b projectsList->SetItem(itemIndex, 2, p.modifyDateString()); projectsList->SetItem(itemIndex, 3, p.path.string()); - //resize columns - int cols = projectsList->GetColumnCount(); - for (int j = 0; j < cols; j++){ - projectsList->SetColumnWidth(j, wxLIST_AUTOSIZE_USEHEADER); + //resize columns, unless the user has set their widths + if (!customColumnWidths){ + int cols = projectsList->GetColumnCount(); + for (int j = 0; j < cols; j++){ + projectsList->SetColumnWidth(j, wxLIST_AUTOSIZE_USEHEADER); + } } // Sort the list to place the new item in the correct position @@ -747,9 +928,13 @@ void MainFrameDerived::OnColumnClick(wxListEvent& event) { } SortProjects(); - // update UI - const bool ascending = projectsList->GetUpdatedAscendingSortIndicator(col); - projectsList->ShowSortIndicator(col, ascending); + // update UI + projectsList->ShowSortIndicator(sortColumn, sortAscending); +} + +void MainFrameDerived::OnColumnResized(wxListEvent& event) { + customColumnWidths = true; + event.Skip(); } // Sort comparison function diff --git a/source/interface_derived.hpp b/source/interface_derived.hpp index 9eb67783..d9b956a1 100644 --- a/source/interface_derived.hpp +++ b/source/interface_derived.hpp @@ -61,6 +61,12 @@ class MainFrameDerived : public MainFrame{ void OnSelectEditorPath(wxCommandEvent&); void OnQuit(wxCommandEvent&); + void OnClose(wxCloseEvent&); + void OnSize(wxSizeEvent&); + void OnMove(wxMoveEvent&); + void RememberWindowRect(); + void OnCenterOnDisplay(wxCommandEvent&); + void KeepOnScreen(); wxWindow* const projectActionItems[3]{ revealProjBtn, @@ -92,6 +98,11 @@ class MainFrameDerived : public MainFrame{ // Sorting state int sortColumn = 0; // Default sort by name (column 0) bool sortAscending = true; // Default ascending + // Set once the user resizes a column; disables autosizing and persists the widths + bool customColumnWidths = false; + + // Last window geometry while not minimized, saved on close + wxRect windowRect; //window events void OnAbout(wxCommandEvent& event); @@ -177,6 +188,7 @@ class MainFrameDerived : public MainFrame{ this->LoadEditorVersions(); } void OnColumnClick(wxListEvent& event); + void OnColumnResized(wxListEvent& event); static int wxCALLBACK CompareItems(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData); void SortProjects(); wxDECLARE_EVENT_TABLE();