# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . from __future__ import print_function # # make sure the Qt rc files are converted into python modules, then load them # this must be done BEFORE other hgview qt5 modules are loaded. import os import os.path as osp from mercurial import pycompat from PyQt5 import uic from PyQt5.QtCore import QTimer, Qt from PyQt5.QtWidgets import QAction, QActionGroup, QMenu, QShortcut from hgviewlib.config import HgConfig from hgviewlib.qt5 import should_rebuild from hgviewlib.qt5 import icon as geticon from hgviewlib.qt5.config import get_font class ActionsMixin(object): """ A Mixin for Hgview widgets containing a simple way to create and register Qt actions. """ def __init__(self, *args, **kwargs): super(ActionsMixin, self).__init__(*args, **kwargs) self._actions = {} self._action_groups = {} self._action_groups_order = [] def _set_action(self, name, **parameters): """Set the registred At action named ``name``. ``parameters`` are optional arguments given to ``add_action``. """ action = self._actions[name] if parameters.get('icon'): action.setIcon(geticon(parameters['icon'])) if parameters.get('tip'): action.setStatusTip(parameters['tip']) if parameters.get('keys'): action.setShortcuts(parameters['keys']) if parameters.get('checked') is not None: action.setCheckable(True) action.setChecked(parameters['checked']) action.setEnabled(parameters.get('enabled', True)) if parameters.get('callback'): if parameters.get('checked') is not None: signal = action.toggled[bool] else: signal = action.triggered signal.connect(parameters['callback']) menu = parameters.get('menu', None) if menu is not None: if not isinstance(menu, pycompat.unicode): # menu might be a QtString ... but it has a safe cast to unicode menu = pycompat.unicode(menu) if menu not in self._action_groups: group = QActionGroup(self) self._action_groups[pycompat.unicode(menu)] = group self._action_groups_order.append(menu) group.addAction(menu).setSeparator(True) else: group = self._action_groups[pycompat.unicode(menu)] group.setExclusive(False) group.addAction(action) def set_action(self, name, **parameters): """Set the registred At action named ``name``. ``parameters`` are optional arguments given to ``add_action``. """ # delayed to not append after add_action QTimer.singleShot(0, lambda: self._set_action(name, **parameters)) def set_actions(self, *names, **parameters): """Same as set_actions but accept multiple registred actions names as positioned arguments. """ for name in names: self.set_action(name, **parameters) def add_action(self, name, action, **options): """Add and attach an action to the widget. The action setup is finalized in a dedicated thread in order to speed up the hgview start up. .. note:: the action is added to the context menu of the widget if the ``menu`` option is specified. Arguments --------- :name: action name :action: a QAction object or a short desciption string. Optional arguments ------------------ :menu: a string containing the group name of the action in the context menu. The action is not added to context menu if set to None (default). :checked: A boolean to set the action as checkable or None. The boolean value is used for the action checked status. (default: None == non checkable). :enabled: A boolean value used for the enabled status (default True) :callback: a slot called on the ``triggered()`` signal (or ``toggled[bool]`` if ``checkable is True) :icon: registred icon name as expected by ``hgviewlib.qt5.icon(icon)`` :keys: a single keybinding or a list of keybindings (see Qt bindings definition) :tip: extended desciption string """ if not isinstance(action, QAction): action = QAction(action, self) self._actions[name] = action self.addAction(action) QTimer.singleShot(0, lambda: self._set_action(name, **options)) return action def get_action(self, name): """Return the registred qt action object named ``name``.""" return self._actions[name] def get_actions(self, *names): """Return a tuple of registred qt actions named ``names``.""" return tuple(self.get_action(name) for name in names) def contextMenuEvent(self, event): """Overwritte context menu event to add registred actions.""" try: menu = self.createStandardContextMenu() except AttributeError: menu = QMenu(self) for name in self._action_groups_order: group = self._action_groups[name] menu.addActions(group.actions()) menu.exec_(event.globalPos()) def ui2cls(ui_name): """Compile the .ui file named ``ui_name`` into a python class en return it. Also try to comvert the .ui file into a .py file in order to generate the python code only once. This allow to speed up the hgview start up as the python module can be imported directly on next start up. """ _path = osp.dirname(__file__) uifile = osp.join(_path, ui_name) pyfile = uifile.replace(".ui", "_ui.py") cmd = 'pyuic5 %s -o %s' % (uifile, pyfile) if should_rebuild(uifile, pyfile): print("Resource files are outdated, running: %s" % cmd) if os.system(cmd) != 0: raise ValueError("Failed rebuilding %s" % ui_name) try: modname = osp.splitext(osp.basename(uifile))[0] + "_ui" modname = "hgviewlib.qt5.%s" % modname mod = __import__(modname, fromlist=['*']) classnames = [x for x in dir(mod) if x.startswith('Ui_')] if len(classnames) == 1: ui_class = getattr(mod, classnames[0]) elif 'Ui_MainWindow' in classnames: ui_class = getattr(mod, 'Ui_MainWindow') else: raise ValueError("Can't determine which main class to use in %s" % modname) except ImportError: ui_class, base_class = uic.loadUiType(uifile) return ui_class class HgDialogMixin(object): """ Mixin for QDialogs defined from a .ui file, which automates the setup of the UI from the ui file, and the loading of user preferences. The main class must define a '_uifile' class attribute. """ def __init__(self, *args, **kwargs): self._cfg = None self._font = None super(HgDialogMixin, self).__init__(*args, **kwargs) def load_ui(self): self.setupUi(self) # we explicitly create a QShortcut so we can disable it # when a "helper context toolbar" is activated (which can be # closed by hitting the Esc shortcut) self.esc_shortcut = QShortcut(self) self.esc_shortcut.setKey(Qt.Key_Escape) self.esc_shortcut.activated.connect(self.maybeClose) self._quickbars = [] self.disab_shortcuts = [] def attachQuickBar(self, qbar): qbar.setParent(self) self._quickbars.append(qbar) qbar.esc_shortcut_disabled[bool].connect(self.setShortcutsEnabled) self.addToolBar(Qt.BottomToolBarArea, qbar) qbar.unhidden.connect(self.ensureOneQuickBar) def setShortcutsEnabled(self, enabled=True): for sh in self.disab_shortcuts: sh.setEnabled(enabled) def ensureOneQuickBar(self): tb = self.sender() for w in self._quickbars: if w is not tb: w.hide() def maybeClose(self): for w in self._quickbars: if w.isVisible(): w.cancel() break else: self.close() def load_config(self, repo): self.cfg = HgConfig(repo.ui) self._font = get_font(self.cfg) self.rowheight = self.cfg.getRowHeight() self.users, self.aliases = self.cfg.getUsers() def accept(self): self.close() def reject(self): self.close()