From 8a78fa71214d48263edd0a89d89c1845b97ebad0 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sun, 27 Nov 2022 01:03:21 +0800 Subject: [PATCH] Feature: Contextual actions for vehicles grouped by shared orders (#8425) --- src/depot_gui.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/group_gui.cpp | 16 ++++++++-------- src/lang/english.txt | 4 ++++ src/order_gui.cpp | 37 ++++++++++++++++++++++++++++++++++++ src/vehicle.cpp | 36 +++++++++++++++++++++++++++++++++++ src/vehicle_func.h | 3 +++ src/vehicle_gui.cpp | 45 +++++++++++++++++++++++++++++++++++--------- src/vehicle_gui.h | 4 ++++ src/window_gui.h | 29 ++++++++++++++++++++++++++-- 9 files changed, 200 insertions(+), 19 deletions(-) diff --git a/src/depot_gui.cpp b/src/depot_gui.cpp index dd87a7929f..7f454946ce 100644 --- a/src/depot_gui.cpp +++ b/src/depot_gui.cpp @@ -24,8 +24,10 @@ #include "tilehighlight_func.h" #include "window_gui.h" #include "vehiclelist.h" +#include "vehicle_func.h" #include "order_backup.h" #include "zoom_func.h" +#include "error.h" #include "depot_cmd.h" #include "train_cmd.h" #include "vehicle_cmd.h" @@ -917,6 +919,49 @@ struct DepotWindow : Window { return true; } + /** + * Clones a vehicle from a vehicle list. If this doesn't make sense (because not all vehicles in the list have the same orders), then it displays an error. + * @return This always returns true, which indicates that the contextual action handled the mouse click. + * Note that it's correct behaviour to always handle the click even though an error is displayed, + * because users aren't going to expect the default action to be performed just because they overlooked that cloning doesn't make sense. + */ + bool OnVehicleSelect(VehicleList::const_iterator begin, VehicleList::const_iterator end) override + { + if (!_ctrl_pressed) { + /* If CTRL is not pressed: If all the vehicles in this list have the same orders, then copy orders */ + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return VehiclesHaveSameEngineList(v1, v2); + })) { + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return VehiclesHaveSameOrderList(v1, v2); + })) { + OnVehicleSelect(*begin); + } else { + ShowErrorMessage(STR_ERROR_CAN_T_BUY_TRAIN + (*begin)->type, STR_ERROR_CAN_T_COPY_ORDER_VEHICLE_LIST, WL_INFO); + } + } else { + ShowErrorMessage(STR_ERROR_CAN_T_BUY_TRAIN + (*begin)->type, STR_ERROR_CAN_T_CLONE_VEHICLE_LIST, WL_INFO); + } + } else { + /* If CTRL is pressed: If all the vehicles in this list share orders, then copy orders */ + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return VehiclesHaveSameEngineList(v1, v2); + })) { + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return v1->FirstShared() == v2->FirstShared(); + })) { + OnVehicleSelect(*begin); + } else { + ShowErrorMessage(STR_ERROR_CAN_T_BUY_TRAIN + (*begin)->type, STR_ERROR_CAN_T_SHARE_ORDER_VEHICLE_LIST, WL_INFO); + } + } else { + ShowErrorMessage(STR_ERROR_CAN_T_BUY_TRAIN + (*begin)->type, STR_ERROR_CAN_T_CLONE_VEHICLE_LIST, WL_INFO); + } + } + + return true; + } + void OnPlaceObjectAbort() override { /* abort clone */ diff --git a/src/group_gui.cpp b/src/group_gui.cpp index a597b1e356..5a423a7cde 100644 --- a/src/group_gui.cpp +++ b/src/group_gui.cpp @@ -884,14 +884,14 @@ public: } case GB_SHARED_ORDERS: { - const Vehicle *v = vehgroup.vehicles_begin[0]; - /* We do not support VehicleClicked() here since the contextual action may only make sense for individual vehicles */ - - if (vindex == v->index) { - if (vehgroup.NumVehicles() == 1) { - ShowVehicleViewWindow(v); - } else { - ShowVehicleListWindow(v); + if (!VehicleClicked(vehgroup)) { + const Vehicle* v = vehgroup.vehicles_begin[0]; + if (vindex == v->index) { + if (vehgroup.NumVehicles() == 1) { + ShowVehicleViewWindow(v); + } else { + ShowVehicleListWindow(v); + } } } break; diff --git a/src/lang/english.txt b/src/lang/english.txt index 3cd979a6e1..8621a3cec9 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -5029,6 +5029,8 @@ STR_ERROR_CAN_T_CHANGE_SERVICING :{WHITE}Can't ch STR_ERROR_VEHICLE_IS_DESTROYED :{WHITE}... vehicle is destroyed +STR_ERROR_CAN_T_CLONE_VEHICLE_LIST :{WHITE}... not all vehicles are identical + STR_ERROR_NO_VEHICLES_AVAILABLE_AT_ALL :{WHITE}No vehicles will be available at all STR_ERROR_NO_VEHICLES_AVAILABLE_AT_ALL_EXPLANATION :{WHITE}Change your NewGRF configuration STR_ERROR_NO_VEHICLES_AVAILABLE_YET :{WHITE}No vehicles are available yet @@ -5055,6 +5057,8 @@ STR_ERROR_CAN_T_SKIP_TO_ORDER :{WHITE}Can't sk STR_ERROR_CAN_T_COPY_SHARE_ORDER :{WHITE}... vehicle can't go to all stations STR_ERROR_CAN_T_ADD_ORDER :{WHITE}... vehicle can't go to that station STR_ERROR_CAN_T_ADD_ORDER_SHARED :{WHITE}... a vehicle sharing this order can't go to that station +STR_ERROR_CAN_T_COPY_ORDER_VEHICLE_LIST :{WHITE}... not all vehicles have the same orders +STR_ERROR_CAN_T_SHARE_ORDER_VEHICLE_LIST :{WHITE}... not all vehicles are sharing orders STR_ERROR_CAN_T_SHARE_ORDER_LIST :{WHITE}Can't share order list... STR_ERROR_CAN_T_STOP_SHARING_ORDER_LIST :{WHITE}Can't stop sharing order list... diff --git a/src/order_gui.cpp b/src/order_gui.cpp index d412a49f7c..816a1b85b3 100644 --- a/src/order_gui.cpp +++ b/src/order_gui.cpp @@ -29,6 +29,9 @@ #include "aircraft.h" #include "engine_func.h" #include "vehicle_func.h" +#include "vehiclelist.h" +#include "vehicle_func.h" +#include "error.h" #include "order_cmd.h" #include "company_cmd.h" @@ -1466,6 +1469,40 @@ public: return true; } + /** + * Clones an order list from a vehicle list. If this doesn't make sense (because not all vehicles in the list have the same orders), then it displays an error. + * @return This always returns true, which indicates that the contextual action handled the mouse click. + * Note that it's correct behaviour to always handle the click even though an error is displayed, + * because users aren't going to expect the default action to be performed just because they overlooked that cloning doesn't make sense. + */ + bool OnVehicleSelect(VehicleList::const_iterator begin, VehicleList::const_iterator end) override + { + bool share_order = _ctrl_pressed || this->goto_type == OPOS_SHARE; + if (this->vehicle->GetNumOrders() != 0 && !share_order) return false; + + if (!share_order) { + /* If CTRL is not pressed: If all the vehicles in this list have the same orders, then copy orders */ + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return VehiclesHaveSameOrderList(v1, v2); + })) { + OnVehicleSelect(*begin); + } else { + ShowErrorMessage(STR_ERROR_CAN_T_COPY_ORDER_LIST, STR_ERROR_CAN_T_COPY_ORDER_VEHICLE_LIST, WL_INFO); + } + } else { + /* If CTRL is pressed: If all the vehicles in this list share orders, then copy orders */ + if (AllEqual(begin, end, [](const Vehicle *v1, const Vehicle *v2) { + return v1->FirstShared() == v2->FirstShared(); + })) { + OnVehicleSelect(*begin); + } else { + ShowErrorMessage(STR_ERROR_CAN_T_SHARE_ORDER_LIST, STR_ERROR_CAN_T_SHARE_ORDER_VEHICLE_LIST, WL_INFO); + } + } + + return true; + } + void OnPlaceObjectAbort() override { this->goto_type = OPOS_NONE; diff --git a/src/vehicle.cpp b/src/vehicle.cpp index eeb22be5be..eab9c1ccaa 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -3013,3 +3013,39 @@ uint32 Vehicle::GetDisplayMinPowerToWeight() const if (max_weight == 0) return 0; return GetGroundVehicleCache()->cached_power * 10u / max_weight; } + +/** + * Checks if two vehicle chains have the same list of engines. + * @param v1 First vehicle chain. + * @param v1 Second vehicle chain. + * @return True if same, false if different. + */ +bool VehiclesHaveSameEngineList(const Vehicle *v1, const Vehicle *v2) +{ + while (true) { + if (v1 == nullptr && v2 == nullptr) return true; + if (v1 == nullptr || v2 == nullptr) return false; + if (v1->GetEngine() != v2->GetEngine()) return false; + v1 = v1->GetNextVehicle(); + v2 = v2->GetNextVehicle(); + } +} + +/** + * Checks if two vehicles have the same list of orders. + * @param v1 First vehicles. + * @param v1 Second vehicles. + * @return True if same, false if different. + */ +bool VehiclesHaveSameOrderList(const Vehicle *v1, const Vehicle *v2) +{ + const Order *o1 = v1->GetFirstOrder(); + const Order *o2 = v2->GetFirstOrder(); + while (true) { + if (o1 == nullptr && o2 == nullptr) return true; + if (o1 == nullptr || o2 == nullptr) return false; + if (!o1->Equals(*o2)) return false; + o1 = o1->next; + o2 = o2->next; + } +} diff --git a/src/vehicle_func.h b/src/vehicle_func.h index aa3a027dfb..13fc352e46 100644 --- a/src/vehicle_func.h +++ b/src/vehicle_func.h @@ -174,4 +174,7 @@ void GetVehicleSet(VehicleSet &set, Vehicle *v, uint8 num_vehicles); void CheckCargoCapacity(Vehicle *v); +bool VehiclesHaveSameEngineList(const Vehicle *v1, const Vehicle *v2); +bool VehiclesHaveSameOrderList(const Vehicle *v1, const Vehicle *v2); + #endif /* VEHICLE_FUNC_H */ diff --git a/src/vehicle_gui.cpp b/src/vehicle_gui.cpp index 467f6731d2..8dc4120268 100644 --- a/src/vehicle_gui.cpp +++ b/src/vehicle_gui.cpp @@ -2018,16 +2018,16 @@ public: case GB_SHARED_ORDERS: { assert(vehgroup.NumVehicles() > 0); - const Vehicle *v = vehgroup.vehicles_begin[0]; - /* We do not support VehicleClicked() here since the contextual action may only make sense for individual vehicles */ - - if (_ctrl_pressed) { - ShowOrdersWindow(v); - } else { - if (vehgroup.NumVehicles() == 1) { - ShowVehicleViewWindow(v); + if (!VehicleClicked(vehgroup)) { + const Vehicle *v = vehgroup.vehicles_begin[0]; + if (_ctrl_pressed) { + ShowOrdersWindow(v); } else { - ShowVehicleListWindow(v); + if (vehgroup.NumVehicles() == 1) { + ShowVehicleViewWindow(v); + } else { + ShowVehicleListWindow(v); + } } } break; @@ -3288,6 +3288,33 @@ bool VehicleClicked(const Vehicle *v) return _thd.GetCallbackWnd()->OnVehicleSelect(v); } +/** + * Dispatch a "vehicle group selected" event if any window waits for it. + * @param begin iterator to the start of the range of vehicles + * @param end iterator to the end of the range of vehicles + * @return did any window accept vehicle group selection? + */ +bool VehicleClicked(VehicleList::const_iterator begin, VehicleList::const_iterator end) +{ + assert(begin != end); + if (!(_thd.place_mode & HT_VEHICLE)) return false; + + /* If there is only one vehicle in the group, act as if we clicked a single vehicle */ + if (begin + 1 == end) return _thd.GetCallbackWnd()->OnVehicleSelect(*begin); + + return _thd.GetCallbackWnd()->OnVehicleSelect(begin, end); +} + +/** + * Dispatch a "vehicle group selected" event if any window waits for it. + * @param vehgroup the GUIVehicleGroup representing the vehicle group + * @return did any window accept vehicle group selection? + */ +bool VehicleClicked(const GUIVehicleGroup &vehgroup) +{ + return VehicleClicked(vehgroup.vehicles_begin, vehgroup.vehicles_end); +} + void StopGlobalFollowVehicle(const Vehicle *v) { Window *w = FindWindowById(WC_MAIN_WINDOW, 0); diff --git a/src/vehicle_gui.h b/src/vehicle_gui.h index 4a750c0477..af8a354e6f 100644 --- a/src/vehicle_gui.h +++ b/src/vehicle_gui.h @@ -12,6 +12,8 @@ #include "window_type.h" #include "vehicle_type.h" +#include "vehicle_gui_base.h" +#include "vehiclelist.h" #include "order_type.h" #include "station_type.h" #include "engine_type.h" @@ -102,6 +104,8 @@ static inline WindowClass GetWindowClassForVehicleType(VehicleType vt) /* Unified window procedure */ void ShowVehicleViewWindow(const Vehicle *v); bool VehicleClicked(const Vehicle *v); +bool VehicleClicked(VehicleList::const_iterator begin, VehicleList::const_iterator end); +bool VehicleClicked(const GUIVehicleGroup &vehgroup); void StartStopVehicle(const Vehicle *v, bool texteffect); Vehicle *CheckClickOnVehicle(const struct Viewport *vp, int x, int y); diff --git a/src/window_gui.h b/src/window_gui.h index 4afe9563de..8bd5dc39ee 100644 --- a/src/window_gui.h +++ b/src/window_gui.h @@ -11,7 +11,10 @@ #define WINDOW_GUI_H #include +#include +#include +#include "vehiclelist.h" #include "vehicle_type.h" #include "viewport_type.h" #include "company_type.h" @@ -681,11 +684,20 @@ public: /** * The user clicked on a vehicle while HT_VEHICLE has been set. - * @param v clicked vehicle. It is guaranteed to be v->IsPrimaryVehicle() == true - * @return True if the click is handled, false if it is ignored. + * @param v clicked vehicle + * @return true if the click is handled, false if it is ignored + * @pre v->IsPrimaryVehicle() == true */ virtual bool OnVehicleSelect(const struct Vehicle *v) { return false; } + /** + * The user clicked on a vehicle while HT_VEHICLE has been set. + * @param v clicked vehicle + * @return True if the click is handled, false if it is ignored + * @pre v->IsPrimaryVehicle() == true + */ + virtual bool OnVehicleSelect(VehicleList::const_iterator begin, VehicleList::const_iterator end) { return false; } + /** * The user cancelled a tile highlight mode that has been set. */ @@ -806,6 +818,19 @@ public: using IterateFromFront = AllWindows; //!< Iterate all windows in Z order from front to back. }; +/** + * Generic helper function that checks if all elements of the range are equal with respect to the given predicate. + * @param begin The start of the range. + * @param end The end of the range. + * @param pred The predicate to use. + * @return True if all elements are equal, false otherwise. + */ +template +inline bool AllEqual(It begin, It end, Pred pred) +{ + return std::adjacent_find(begin, end, std::not_fn(pred)) == end; +} + /** * Get the nested widget with number \a widnum from the nested widget tree. * @tparam NWID Type of the nested widget.