def init_widget(self): super().init_widget() container = ABox(position=(20, 210)) icon = Icon(name='build_all_bg') button = ImageButton(name='build_all_button') container.addChild(icon) container.addChild(button) self.widget.addChild(container) self.update_data()
def __init_gui(self): self.gui = ABox() self.label = Label(position=(10, 5)) self.bg = TooltipBG() self.gui.addChildren(self.bg, self.label)
class _Tooltip: """Base class for pychan widgets overloaded with tooltip functionality""" # Character count after which we start new line. CHARS_PER_LINE = 19 # Find and replace horribly complicated elements that allow simple icons. icon_regexp = re.compile(r'\[\[Buildmenu((?: \d+\:\d+)+)\]\]') def init_tooltip(self): # the widget's parent's parent's ..... until the topmost self.topmost_widget = None self.gui = None self.bg = None self.label = None self.mapEvents({ self.name + '/mouseEntered/tooltip': self.position_tooltip, self.name + '/mouseExited/tooltip': self.hide_tooltip, # Below causes frequent Segmentation Faults due to too many # self.position_tooltip() calls. # self.name + '/mouseMoved/tooltip' : self.position_tooltip, # TIP: the mousePressed event is especially useful when such as click # will trigger this tooltip's parent widget to be hidden (or destroyed), # which hides this tooltip first before hides the parent widget. # Otherwise the tooltip will show forever. self.name + '/mousePressed/tooltip': self.hide_tooltip, # TODO: not sure if below are useful or not # self.name + '/mouseReleased/tooltip' : self.position_tooltip, self.name + '/mouseDragged/tooltip': self.hide_tooltip }) self.tooltip_shown = False self.cooldown = time.time() # initial timer value def __init_gui(self): self.gui = ABox() self.label = Label(position=(10, 5)) self.bg = TooltipBG() self.gui.addChildren(self.bg, self.label) def position_tooltip(self, event): """Calculates a nice position for the tooltip. @param event: mouse event from fife or tuple screenpoint """ if not self.helptext: return # TODO: think about nicer way of handling the polymorphism here, # e.g. a position_tooltip_event and a position_tooltip_tuple where = event # fife forces this to be called event, but here it can also be a tuple if isinstance(where, tuple): x, y = where else: if where.getButton() == fife.MouseEvent.MIDDLE: return x, y = where.getX(), where.getY() if self.gui is None: self.__init_gui() widget_position = self.getAbsolutePos() # Sometimes, we get invalid events from pychan, it is probably related to changing the # gui when the mouse hovers on gui elements. # Random tests have given evidence to believe that pychan indicates invalid events # by setting the top container's position to 0, 0. # Since this position is currently unused, it can serve as invalid flag, # and dropping these events seems to lead to the desired placements def get_top(w): return get_top(w.parent) if w.parent else w top_pos = get_top(self).position if top_pos == (0, 0): return if not self.tooltip_shown: self.show_tooltip() #ExtScheduler().add_new_object(self.show_tooltip, self, run_in=0.3, loops=0) self.tooltip_shown = True screen_width = horizons.globals.fife.engine_settings.getScreenWidth() self.gui.y = widget_position[1] + y + 5 offset = x + 10 if (widget_position[0] + self.gui.size[0] + offset) > screen_width: # right screen edge, position to the left of cursor instead offset = x - self.gui.size[0] - 5 self.gui.x = widget_position[0] + offset def show_tooltip(self): if not self.helptext: return if self.gui is None: self.__init_gui() # Compare and reset timer value if difference from current time shorter than X sec. if (time.time() - self.cooldown) < 1: return else: self.cooldown = time.time() #HACK: support icons in build menu # Code below exists for the sole purpose of build menu tooltips showing # resource icons. Even supporting that is a pain (as you will see), # so if you think you need icons in other tooltips, maybe reconsider. # [These unicode() calls brought to you by status icon tooltip code.] buildmenu_icons = self.icon_regexp.findall(str(self.helptext)) # Remove the weird stuff before displaying text. replaced = self.icon_regexp.sub('', str(self.helptext)) # Specification looks like [[Buildmenu 1:250 4:2 6:2]] if buildmenu_icons: hbox = HBox(position=(7, 5)) for spec in buildmenu_icons[0].split(): (res_id, amount) = spec.split(':') label = Label(text=amount + ' ') icon = Icon(image=get_res_icon_path(int(res_id)), size=(16, 16), scale=True) hbox.addChildren(icon, label) hbox.adaptLayout() # Now display the 16x16px "required resources" icons in the last line. self.gui.addChild(hbox) #HACK: wrap tooltip text # This looks better than splitting into several lines and joining them. # It works because replace_whitespace in `fill` defaults to True. replaced = replaced.replace(r'\n', self.CHARS_PER_LINE * ' ') replaced = replaced.replace('[br]', self.CHARS_PER_LINE * ' ') tooltip = textwrap.fill(replaced, self.CHARS_PER_LINE) # Finish up the actual tooltip (text, background panel amount, layout). # To display build menu icons, we need another empty (first) line. self.bg.amount = len(tooltip.splitlines()) - 1 + bool(buildmenu_icons) self.label.text = bool(buildmenu_icons) * '\n' + tooltip self.gui.adaptLayout() self.gui.show() # NOTE: the below code in this method is a hack to resolve #2227 # cannot find a better way to fix it, cause in fife.pychan, it seems # if a widget gets hidden or removed, the children of that widget are not # hidden or removed properly (at least in Python code) # update topmost_widget every time the tooltip is shown # this is to dismiss the tooltip later, see _check_hover_alive target_widget = self while target_widget: self.topmost_widget = target_widget target_widget = target_widget.parent # add an event to constantly check whether the hovered widget is still there # if this is no longer there, dismiss the tooltip widget ExtScheduler().add_new_object(self._check_hover_alive, self, run_in=0.5, loops=-1) def _check_hover_alive(self): target_widget = self # traverse the widget chain again while target_widget: # none of ancestors of this widget gets removed, # just do nothing and let the tooltip shown if target_widget == self.topmost_widget: return # one ancestor of this widget is hidden if not target_widget.isVisible(): self.hide_tooltip() return target_widget = target_widget.parent # if it comes to here, meaning one ancestor of this widget is removed self.hide_tooltip() def hide_tooltip(self, event=None): if self.gui is not None: self.gui.hide() # tooltip is hidden, no need to check any more ExtScheduler().rem_call(self, self._check_hover_alive) self.topmost_widget = None self.tooltip_shown = False
class _Tooltip: """Base class for pychan widgets overloaded with tooltip functionality""" # Character count after which we start new line. CHARS_PER_LINE = 19 # Find and replace horribly complicated elements that allow simple icons. icon_regexp = re.compile(r'\[\[Buildmenu((?: \d+\:\d+)+)\]\]') def init_tooltip(self): # the widget's parent's parent's ..... until the topmost self.topmost_widget = None self.gui = None self.bg = None self.label = None self.mapEvents({ self.name + '/mouseEntered/tooltip': self.position_tooltip, self.name + '/mouseExited/tooltip': self.hide_tooltip, # Below causes frequent Segmentation Faults due to too many # self.position_tooltip() calls. # self.name + '/mouseMoved/tooltip' : self.position_tooltip, # TIP: the mousePressed event is especially useful when such as click # will trigger this tooltip's parent widget to be hidden (or destroyed), # which hides this tooltip first before hides the parent widget. # Otherwise the tooltip will show forever. self.name + '/mousePressed/tooltip': self.hide_tooltip, # TODO: not sure if below are useful or not # self.name + '/mouseReleased/tooltip' : self.position_tooltip, self.name + '/mouseDragged/tooltip': self.hide_tooltip }) self.tooltip_shown = False self.cooldown = time.time() # initial timer value def __init_gui(self): self.gui = ABox() self.label = Label(position=(10, 5)) self.bg = TooltipBG() self.gui.addChildren(self.bg, self.label) def position_tooltip(self, event): """Calculates a nice position for the tooltip. @param event: mouse event from fife or tuple screenpoint """ if not self.helptext: return # TODO: think about nicer way of handling the polymorphism here, # e.g. a position_tooltip_event and a position_tooltip_tuple where = event # fife forces this to be called event, but here it can also be a tuple if isinstance(where, tuple): x, y = where else: if where.getButton() == fife.MouseEvent.MIDDLE: return x, y = where.getX(), where.getY() if self.gui is None: self.__init_gui() widget_position = self.getAbsolutePos() # Sometimes, we get invalid events from pychan, it is probably related to changing the # gui when the mouse hovers on gui elements. # Random tests have given evidence to believe that pychan indicates invalid events # by setting the top container's position to 0, 0. # Since this position is currently unused, it can serve as invalid flag, # and dropping these events seems to lead to the desired placements def get_top(w): return get_top(w.parent) if w.parent else w top_pos = get_top(self).position if top_pos == (0, 0): return if not self.tooltip_shown: self.show_tooltip() #ExtScheduler().add_new_object(self.show_tooltip, self, run_in=0.3, loops=0) self.tooltip_shown = True screen_width = horizons.globals.fife.engine_settings.getScreenWidth() if not isinstance(self, Icon): # exclude building statusicons (eg low productivity) and minimap # If the a button spawn a tooltip and the cursor hovers over that # tooltip the tooltip will disappear and the button becomes # unclickable. (see issue #2776: https://git.io/vxRrn) # This is a workaround for that problem, by making tooltips for # buttons always display below the button. # There is a small chance that it still happens if you move the # cursor very fast, but that seems unlikely to be such a major # problem as it used to be. # The underlying problem is that the cursor can not be repositioned # on every mousemove because that causes frequent segfaults. # If that problem would be fixed this workaround wouldn't be # neccessary anymore. ypos = widget_position[1] + self.height + 10 xpos = int(widget_position[0]) xpos = min(xpos, screen_width - self.gui.size[0] - 5) else: ypos = widget_position[1] + y + 5 offset = x + 10 if (widget_position[0] + self.gui.size[0] + offset) > screen_width: # right screen edge, position to the left of cursor instead offset = x - self.gui.size[0] - 5 xpos = widget_position[0] + offset self.gui.x = xpos self.gui.y = ypos def show_tooltip(self): if not self.helptext: return if self.gui is None: self.__init_gui() # Compare and reset timer value if difference from current time shorter than X sec. if (time.time() - self.cooldown) < .1: return else: self.cooldown = time.time() #HACK: support icons in build menu # Code below exists for the sole purpose of build menu tooltips showing # resource icons. Even supporting that is a pain (as you will see), # so if you think you need icons in other tooltips, maybe reconsider. # [These unicode() calls brought to you by status icon tooltip code.] buildmenu_icons = self.icon_regexp.findall(str(self.helptext)) # Remove the weird stuff before displaying text. replaced = self.icon_regexp.sub('', str(self.helptext)) # Specification looks like [[Buildmenu 1:250 4:2 6:2]] if buildmenu_icons: hbox = HBox(position=(7, 5)) for spec in buildmenu_icons[0].split(): (res_id, amount) = spec.split(':') label = Label(text=amount + ' ') icon = Icon(image=get_res_icon_path(int(res_id)), size=(16, 16), scale=True) hbox.addChildren(icon, label) hbox.adaptLayout() # Now display the 16x16px "required resources" icons in the last line. self.gui.addChild(hbox) #HACK: wrap tooltip text # This looks better than splitting into several lines and joining them. # It works because replace_whitespace in `fill` defaults to True. replaced = replaced.replace(r'\n', self.CHARS_PER_LINE * ' ') replaced = replaced.replace('[br]', self.CHARS_PER_LINE * ' ') tooltip = textwrap.fill(replaced, self.CHARS_PER_LINE) # Finish up the actual tooltip (text, background panel amount, layout). # To display build menu icons, we need another empty (first) line. self.bg.amount = len(tooltip.splitlines()) - 1 + bool(buildmenu_icons) self.label.text = bool(buildmenu_icons) * '\n' + tooltip self.gui.adaptLayout() self.gui.show() # NOTE: the below code in this method is a hack to resolve #2227 # cannot find a better way to fix it, cause in fife.pychan, it seems # if a widget gets hidden or removed, the children of that widget are not # hidden or removed properly (at least in Python code) # update topmost_widget every time the tooltip is shown # this is to dismiss the tooltip later, see _check_hover_alive target_widget = self while target_widget: self.topmost_widget = target_widget target_widget = target_widget.parent # add an event to constantly check whether the hovered widget is still there # if this is no longer there, dismiss the tooltip widget ExtScheduler().add_new_object(self._check_hover_alive, self, run_in=0.5, loops=-1) def _check_hover_alive(self): target_widget = self # traverse the widget chain again while target_widget: # none of ancestors of this widget gets removed, # just do nothing and let the tooltip shown if target_widget == self.topmost_widget: return # one ancestor of this widget is hidden if not target_widget.isVisible(): self.hide_tooltip() return target_widget = target_widget.parent # if it comes to here, meaning one ancestor of this widget is removed self.hide_tooltip() def hide_tooltip(self, event=None): if self.gui is not None: self.gui.hide() # tooltip is hidden, no need to check any more ExtScheduler().rem_call(self, self._check_hover_alive) self.topmost_widget = None self.tooltip_shown = False