Skip to content
Open
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
213 changes: 199 additions & 14 deletions source/interface_derived.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
#include <wx/dirdlg.h>
#include <wx/aboutdlg.h>
#include <wx/config.h>
#include <wx/display.h>

#include <format>
#include <limits>

#include <filesystem>
#if __APPLE__
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -74,11 +89,11 @@ wxEND_EVENT_TABLE()

//call superclass constructor
MainFrameDerived::MainFrameDerived() : MainFrame(NULL){

// load prefs
wxConfig config(config_name.data());

sortColumn = std::clamp<long>(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
{
Expand All @@ -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<int> 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);
Expand All @@ -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();
}
Expand All @@ -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<long long>(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<unsigned>(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<long long>::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();
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions source/interface_derived.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Loading