Package horizons :: Package gui :: Package mousetools :: Module selectiontool
[hide private]
[frames] | no frames]

Source Code for Module horizons.gui.mousetools.selectiontool

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import traceback 
 23   
 24  from fife import fife 
 25   
 26  from horizons.command.unit import Act 
 27  from horizons.component.selectablecomponent import SelectableComponent 
 28  from horizons.constants import LAYERS 
 29  from horizons.gui.mousetools.navigationtool import NavigationTool 
 30  from horizons.util.worldobject import WorldObject, WorldObjectNotFound 
 31   
 32   
33 -class SelectionTool(NavigationTool):
34 _SELECTION_RECTANGLE_NAME = "_select" # GenericRenderer objects are sorted by name, so first char is important 35
36 - def __init__(self, session):
37 super().__init__(session) 38 self.deselect_at_end = True # Set this to deselect selections while exiting SelectionTool
39
40 - def remove(self):
41 # Deselect if needed while exiting 42 if self.deselect_at_end: 43 selectables = self.filter_selectable(self.session.selected_instances) 44 for i in self.filter_component(SelectableComponent, selectables): 45 i.deselect() 46 super().remove()
47
48 - def is_selectable(self, entity):
49 # also enemy entities are selectable, but the selection representation will differ 50 return entity.has_component(SelectableComponent)
51
52 - def filter_component(self, component, instances):
53 """Only get specific component from a list of world objects""" 54 return [instance.get_component(component) for instance in instances]
55
56 - def filter_selectable(self, instances):
57 """Only keeps selectables from a list of world objects""" 58 return list(filter(self.is_selectable, instances))
59
60 - def is_owned_by_player(self, instance):
61 """Returns boolean if single world object is owned by local player""" 62 return instance.owner is not None and \ 63 hasattr(instance.owner, "is_local_player") and \ 64 instance.owner.is_local_player
65
66 - def filter_owner(self, instances):
67 """Only keep instances belonging to the user. This is used for multiselection""" 68 return [i for i in instances if self.is_owned_by_player(i)]
69
70 - def fife_instance_to_uh_instance(self, instance):
71 """Visual fife instance to uh game logic object or None""" 72 i_id = instance.getId() 73 if i_id == '': 74 return None 75 try: 76 return WorldObject.get_object_by_id(int(i_id)) 77 except WorldObjectNotFound: 78 return None
79
80 - def mouseDragged(self, evt):
81 if evt.getButton() == fife.MouseEvent.LEFT and hasattr(self, 'select_begin'): 82 x, y = self.select_begin 83 xx, yy = evt.getX(), evt.getY() 84 do_multi = ((x - xx) ** 2 + (y - yy) ** 2) >= 10 # from 3px (3*3 + 1) 85 self.session.view.renderer['GenericRenderer'].removeAll(self.__class__._SELECTION_RECTANGLE_NAME) 86 if do_multi: 87 # draw a rectangle 88 xmin, xmax = min(x, xx), max(x, xx) 89 ymin, ymax = min(y, yy), max(y, yy) 90 a = fife.Point(xmin, ymin) 91 b = fife.Point(xmax, ymin) 92 c = fife.Point(xmax, ymax) 93 d = fife.Point(xmin, ymax) 94 self._draw_rect_line(a, b) 95 self._draw_rect_line(b, c) 96 self._draw_rect_line(d, c) 97 self._draw_rect_line(d, a) 98 area = fife.Rect(xmin, ymin, xmax - xmin, ymax - ymin) 99 else: 100 area = fife.ScreenPoint(xx, yy) 101 instances = self.session.view.cam.getMatchingInstances( 102 area, 103 self.session.view.layers[LAYERS.OBJECTS], 104 False) # False for accurate 105 106 # get selection components 107 instances = (self.fife_instance_to_uh_instance(i) for i in instances) 108 instances = [i for i in instances if i is not None] 109 110 # We only consider selectable items when dragging a selection box. 111 instances = self.filter_selectable(instances) 112 113 # If there is at least one player unit, we don't select any enemies. 114 # This applies to both buildings and ships. 115 if any((self.is_owned_by_player(instance) for instance in instances)): 116 instances = self.filter_owner(instances) 117 118 self._update_selection(instances, do_multi) 119 120 elif evt.getButton() == fife.MouseEvent.RIGHT: 121 pass 122 else: 123 super().mouseDragged(evt) 124 return 125 evt.consume()
126
127 - def mouseReleased(self, evt):
128 if evt.getButton() == fife.MouseEvent.LEFT and hasattr(self, 'select_begin'): 129 self.apply_select() 130 del self.select_begin, self.select_old 131 self.session.view.renderer['GenericRenderer'].removeAll(self.__class__._SELECTION_RECTANGLE_NAME) 132 elif evt.getButton() == fife.MouseEvent.RIGHT: 133 pass 134 else: 135 super().mouseReleased(evt) 136 return 137 evt.consume()
138
139 - def apply_select(self):
140 """ 141 Called when selected instances changes. (Shows their menu) 142 Does not do anything when nothing is selected, i.e. doesn't hide their menu. 143 If one of the selected instances can attack, switch mousetool to AttackingTool. 144 """ 145 if self.session.world.health_visible_for_all_health_instances: 146 self.session.world.toggle_health_for_all_health_instances() 147 selected = self.session.selected_instances 148 if not selected: 149 return 150 if len(selected) == 1: 151 next(iter(selected)).get_component(SelectableComponent).show_menu() 152 else: 153 self.session.ingame_gui.show_multi_select_tab(selected) 154 155 # local import to prevent cycle 156 from horizons.gui.mousetools.attackingtool import AttackingTool 157 # change session cursor to attacking tool if selected instances can attack 158 found_military = any(hasattr(i, 'attack') and i.owner.is_local_player 159 for i in selected) 160 # Handover to AttackingTool without deselecting 161 self.deselect_at_end = not found_military 162 163 if found_military and not isinstance(self.session.ingame_gui.cursor, AttackingTool): 164 self.session.ingame_gui.set_cursor('attacking') 165 if not found_military and isinstance(self.session.ingame_gui.cursor, AttackingTool): 166 self.session.ingame_gui.set_cursor('default')
167
168 - def mousePressed(self, evt):
169 if evt.isConsumedByWidgets(): 170 super().mousePressed(evt) 171 return 172 elif evt.getButton() == fife.MouseEvent.LEFT: 173 if self.session.selected_instances is None: 174 # this is a very odd corner case, it should only happen after the session has been ended 175 # we can't allow to just let it crash however 176 self.log.error('Error: selected_instances is None. Please report this!') 177 traceback.print_stack() 178 self.log.error('Error: selected_instances is None. Please report this!') 179 return 180 instances = self.get_hover_instances(evt) 181 self.select_old = frozenset(self.session.selected_instances) if evt.isControlPressed() else frozenset() 182 183 instances = list(filter(self.is_selectable, instances)) 184 # On single click, only one building should be selected from the hover_instances. 185 # The if is for [] and [single_item] cases (they crashed). 186 # It acts as user would expect: instances[0] selects buildings in front first. 187 instances = instances if len(instances) <= 1 else [instances[0]] 188 189 self._update_selection(instances) 190 191 self.select_begin = (evt.getX(), evt.getY()) 192 self.session.ingame_gui.hide_menu() 193 elif evt.getButton() == fife.MouseEvent.RIGHT: 194 target_mapcoord = self.get_exact_world_location(evt) 195 for i in self.session.selected_instances: 196 if i.movable: 197 Act(i, target_mapcoord.x, target_mapcoord.y).execute(self.session) 198 else: 199 super().mousePressed(evt) 200 return 201 evt.consume()
202
203 - def _draw_rect_line(self, start, end):
204 renderer = self.session.view.renderer['GenericRenderer'] 205 renderer.addLine(self.__class__._SELECTION_RECTANGLE_NAME, 206 fife.RendererNode(start), fife.RendererNode(end), 207 200, 200, 200)
208
209 - def _update_selection(self, instances, do_multi=False):
210 """ 211 self.select_old are old instances still relevant now (esp. on ctrl) 212 @param instances: uh instances 213 @param do_multi: True if selection rectangle on drag is used 214 """ 215 self.log.debug("update selection %s", [str(i) for i in instances]) 216 217 if do_multi: # add to selection 218 instances = self.select_old.union(instances) 219 else: # this is for deselecting among a selection with ctrl 220 instances = self.select_old.symmetric_difference(instances) 221 222 # sanity: 223 # - no multiple entities from enemy selected 224 if len(instances) > 1: 225 user_instances = self.filter_owner(instances) 226 if user_instances: # check at least one remaining 227 instances = user_instances 228 else: 229 instances = [next(iter(instances))] 230 selectable = frozenset(self.filter_component(SelectableComponent, instances)) 231 232 # apply changes 233 selected_components = set(self.filter_component(SelectableComponent, 234 self.filter_selectable(self.session.selected_instances))) 235 for sel_comp in selected_components - selectable: 236 sel_comp.deselect() 237 for sel_comp in selectable - selected_components: 238 sel_comp.select() 239 240 self.session.selected_instances = {i.instance for i in selectable}
241