How to using zoom Ctrl + MMB like in Photoshop?

There is KisConfig entry called zoomHorizontal=bool, but looks like it’s ignored in actual zooming code.

Any way here is plugin that overrides zooming action. Sadly it will not center zoom to cursors initial position.

Just copy & paste code text to correct file paths, and enable plugin from Kritas settings + restart Krita. There should be new menu entry ToolsShow Cursor Zoomer Settings, that allows to change zoomer settings.

file tree structure

pykrita/
    cursor_zoomer_plugin/
        __init__.py
        cursor_zoomer_extension.py
    cursor_zoomer_plugin.desktop

cursor_zoomer_plugin/__init__.py

from krita import Krita


PLUGIN_NAME = __name__


def register():
    from .cursor_zoomer_extension import (
            CursorZoomerExtension,)

    app = Krita.instance()
    extension = CursorZoomerExtension(app)
    extension.setObjectName(f'{PLUGIN_NAME}:cursor_zoomer_extension')
    app.addExtension(extension)


register()

cursor_zoomer_plugin/cursor_zoomer_extension.py

from krita import (
        Krita,
        Extension)

from PyQt5.QtCore import (
        Qt,
        pyqtSlot as QSlot,
        pyqtSignal as QSignal,
        pyqtProperty as QProperty,
        QSize,
        QEvent)

from PyQt5.QtGui import (
        QKeySequence,)

from PyQt5.QtWidgets import (
        QWidget,
        QComboBox,
        QPushButton,
        QDoubleSpinBox,
        QFormLayout,
        QHBoxLayout,
        QApplication,
        QMessageBox)

from cursor_zoomer_plugin import (
        PLUGIN_NAME,)


class CursorZoomerExtension(Extension):
    def setup(self):
        self._button = Qt.MiddleButton
        self._modifiers = Qt.KeyboardModifiers(Qt.ControlModifier)
        self._axis = 'x'
        self._multiplier = 1.0
        self._editor = None
        self._zoomies_start_pos = None
        self._zoomies_start_level = None

    def createActions(self, window):
        show_editor_action = window.createAction(
                f'{PLUGIN_NAME}:show_editor_action',
                'Show Cursor Zoomer Settings',
                'tools')
        show_editor_action.triggered.connect(self._on_show_editor_triggered)
        self.read_settings()

    def _on_show_editor_triggered(self, checked=None):
        if self._editor is None:
            self._editor = CursorZoomerSettingsEditor()
        self._editor.show()
        self._editor.raise_()

    def read_settings(self):
        self._button, self._modifiers, self._axis, self._multiplier = read_settings()
        QApplication.instance().installEventFilter(self)

    def eventFilter(self, obj, event):
        e_type = event.type()

        if e_type in {QEvent.MouseButtonPress, QEvent.TabletPress, QEvent.MouseButtonRelease, QEvent.TabletRelease}:
            if ((event.buttons() == self._button) and (event.modifiers() == self._modifiers)):
                self._zoomies_start(event.globalPos())  # .globalPosition() in Qt6
                return True  # eat the event!
            else:
                # buttons is not ONLY self.button, disable zoomies
                self._zoomies_end()
                # don't eat the event
        elif (e_type in {QEvent.MouseMove, QEvent.TabletMove}
                    and (event.buttons() == self._button)
                    and (event.modifiers() == self._modifiers)):
            self._zoomies(event.globalPos())  # .globalPosition() in Qt6
            return True  # eat the event!

        return super().eventFilter(obj, event)

    def _zoomies_start(self, pos):
        app = Krita.instance()
        window = app.activeWindow()
        view = window.activeView()
        canvas = view.canvas()
        self._zoomies_start_pos = pos
        self._zoomies_start_level = canvas.zoomLevel()

    def _zoomies_end(self):
        self._zoomies_start_pos = None
        self._zoomies_start_level = None

    def _zoomies(self, pos):
        if (self._zoomies_start_pos is None) or (self._zoomies_start_level is None):
            return  # something strange.

        level_offset = 0.0
        if self._axis == 'x':
            level_offset = 0.001 * self._multiplier * (self._zoomies_start_pos.x() - pos.x())
        elif self._axis == 'y':
            level_offset = 0.001 * self._multiplier * (self._zoomies_start_pos.y() - pos.y())

        app = Krita.instance()
        window = app.activeWindow()
        view = window.activeView()
        canvas = view.canvas()
        canvas.setZoomLevel(self._zoomies_start_level + level_offset)


class ModifiersEdit(QWidget):
    modifiers_changed = QSignal(Qt.KeyboardModifiers)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        layout = QHBoxLayout()
        self.setLayout(layout)
        self._shift_button = QPushButton(text='Shift', checkable=True)
        layout.addWidget(self._shift_button)
        self._ctrl_button = QPushButton(text='Ctrl', checkable=True)
        layout.addWidget(self._ctrl_button)
        self._alt_button = QPushButton(text='Alt', checkable=True)
        layout.addWidget(self._alt_button)
        self._meta_button = QPushButton(text='Meta', checkable=True)
        layout.addWidget(self._meta_button)

    def get_modifiers(self):
        modifiers = Qt.KeyboardModifiers(
                self._shift_button.isChecked() * Qt.ShiftModifier
                | self._ctrl_button.isChecked() * Qt.ControlModifier
                | self._alt_button.isChecked() * Qt.AltModifier
                | self._meta_button.isChecked() * Qt.MetaModifier)
        return modifiers

    @QSlot(Qt.KeyboardModifiers)
    def set_modifiers(self, new_modifiers):
        new_modifiers = Qt.KeyboardModifiers(new_modifiers)
        old_modifiers = self.get_modifiers()
        if new_modifiers == old_modifiers:
            return  # nothing to do
        if new_modifiers & Qt.ShiftModifier:
            self._shift_button.setChecked(True)
        if new_modifiers & Qt.ControlModifier:
            self._ctrl_button.setChecked(True)
        if new_modifiers & Qt.AltModifier:
            self._alt_button.setChecked(True)
        if new_modifiers & Qt.MetaModifier:
            self._meta_button.setChecked(True)
        self.modifiers_changed.emit(new_modifiers)

    modifiers = QProperty(
            Qt.KeyboardModifiers,
            fget=get_modifiers,
            fset=set_modifiers,
            notify=modifiers_changed,
            user=True)


class CursorZoomerSettingsEditor(QWidget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.setWindowTitle('Cursor Zoomer Settings')

        layout = QFormLayout()
        self.setLayout(layout)
        self._button_selector = QComboBox(editable=False)
        for name, button_enum in mouse_buttons().items():
            self._button_selector.addItem(name, button_enum)
        layout.addRow('button', self._button_selector)
        self._modifiers_edit = ModifiersEdit()
        layout.addRow('modifiers', self._modifiers_edit)
        self._axis_selector = QComboBox()
        self._axis_selector.addItem('x', 'x')
        self._axis_selector.addItem('y', 'y')
        layout.addRow('axis', self._axis_selector)
        self._multiplier_spin = QDoubleSpinBox(minimum=-10000.0, maximum=10000.0, value=1.0)
        layout.addRow('multiplier', self._multiplier_spin)

        self._button_selector.currentIndexChanged[int].connect(lambda _: self.write_settings())
        self._modifiers_edit.modifiers_changed.connect(lambda _: self.write_settings())
        self._axis_selector.currentIndexChanged[int].connect(lambda _: self.write_settings())
        self._multiplier_spin.valueChanged.connect(lambda _: self.write_settings())

    def write_settings(self):
        write_settings(
                self._button_selector.currentData(),
                self._modifiers_edit.modifiers,
                self._axis_selector.currentData(),
                self._multiplier_spin.value())
        self.refresh_extension()

    def refresh_extension(self):
        app = Krita.instance()
        extension = next((e for e in app.extensions() if isinstance(e, CursorZoomerExtension)), None)
        extension.read_settings()

    def read_settings(self):
        button, modifiers, axis, multiplier = read_settings()
        button_index = self._button_selector.findData(button)
        self._button_selector.setCurrentIndex(button_index)
        self._modifiers_edit.modifiers = modifiers
        axis_index = self._axis_selector.findData(axis)
        self._axis_selector.setCurrentIndex(axis_index)
        self._multiplier_spin.setValue(multiplier)

    def showEvent(self, event):
        super().showEvent(event)
        self.read_settings()


def write_settings(button, modifiers, axis, multiplier):
    app = Krita.instance()
    app.writeSetting(PLUGIN_NAME, 'button', get_mouse_button_name(button))
    app.writeSetting(PLUGIN_NAME, 'modifiers', modifiers_to_string(modifiers))
    app.writeSetting(PLUGIN_NAME, 'axis', str(axis))
    app.writeSetting(PLUGIN_NAME, 'multiplier', str(multiplier))


def read_settings():
    app = Krita.instance()
    button = get_mouse_button_enum(app.readSetting(PLUGIN_NAME, 'button', 'MiddleButton'))
    modifiers = string_to_modifiers(app.readSetting(PLUGIN_NAME, 'modifiers', 'Ctrl'))
    axis = app.readSetting(PLUGIN_NAME, 'axis', 'x').strip().lower()
    multiplier = float(app.readSetting(PLUGIN_NAME, 'multiplier', '1.0'))
    return button, modifiers, axis, multiplier


def mouse_buttons():
    mouse_buttons = dict()
    for name in dir(Qt):
        value = getattr(Qt, name)
        if isinstance(value, Qt.MouseButton):
            mouse_buttons[name] = value
    return mouse_buttons


def get_mouse_button_name(button_enum):
    for name in dir(Qt):
        value = getattr(Qt, name)
        if isinstance(value, Qt.MouseButton) and (value == button_enum):
            return name


def get_mouse_button_enum(button_name):
    for name in dir(Qt):
        value = getattr(Qt, name)
        if isinstance(value, Qt.MouseButton) and (name == button_name):
            return value


def modifiers_to_string(modifiers):
    modifiers_string = '+'.join(token for token, a_modifier in (
                ('Ctrl', Qt.ControlModifier),
                ('Alt', Qt.AltModifier),
                ('Shift', Qt.ShiftModifier),
                ('Meta', Qt.MetaModifier))
            if a_modifier & modifiers)
    return modifiers_string


def string_to_modifiers(modifiers_string):
    modifiers = Qt.KeyboardModifiers()
    tokens = {token.strip().casefold() for token in modifiers_string.split('+')}
    for a_modifier, token in (
            (Qt.ControlModifier, 'Ctrl'.casefold()),
            (Qt.AltModifier, 'Alt'.casefold()),
            (Qt.ShiftModifier, 'Shift'.casefold()),
            (Qt.MetaModifier, 'Meta'.casefold())):
        if token in tokens:
            modifiers |= a_modifier
    return modifiers

cursor_zoomer_plugin.desktop

[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=cursor_zoomer_plugin
X-Python-2-Compatible=false
X-Krita-Manual=readme.md
Name=Cursor Zoomer Plugin
Comment=Adds Cursor Zoomer to Krita (settings for zooming to documents using mouse axis with button + modifiers

Ps: settings will allow you to brick your Krita (buttons = All buttons, modifiers = None) so be gentle! (if you brick your Krita, open kritarc in text editor and edit / remove [cursor_zoomer_plugin] section)

/AkiR

2 Likes