diff --git a/src/scripts/CMakeLists.txt b/src/scripts/CMakeLists.txt index 4f9526c83..d7eda83b7 100644 --- a/src/scripts/CMakeLists.txt +++ b/src/scripts/CMakeLists.txt @@ -6,3 +6,4 @@ function(install_python_script name) endfunction() install_python_script(hellopython) +install_python_script(runaction) diff --git a/src/scripts/Messages.sh b/src/scripts/Messages.sh index 38a0dcb6c..17f258b72 100644 --- a/src/scripts/Messages.sh +++ b/src/scripts/Messages.sh @@ -8,4 +8,11 @@ XGETTEXT_FLAGS_PYTHON="\ -ki18n:1 -ki18np:1,2 \ " -$XGETTEXT_PROGRAM $XGETTEXT_FLAGS_PYTHON `find hellopython -name '*.py'` -o $podir/falkon_hellopython.pot +python_scripts=" +hellopython +runaction +" + +for script in $python_scripts; do + $XGETTEXT_PROGRAM $XGETTEXT_FLAGS_PYTHON `find $script -name '*.py'` -o $podir/falkon_$script.pot +done diff --git a/src/scripts/runaction/__init__.py b/src/scripts/runaction/__init__.py new file mode 100644 index 000000000..1949bc144 --- /dev/null +++ b/src/scripts/runaction/__init__.py @@ -0,0 +1,18 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 .runaction import * diff --git a/src/scripts/runaction/action.py b/src/scripts/runaction/action.py new file mode 100644 index 000000000..e76dae80d --- /dev/null +++ b/src/scripts/runaction/action.py @@ -0,0 +1,73 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 . +# ============================================================ +import Falkon +import os, re, enum, shlex +from PySide2 import QtCore, QtGui + +class Action(): + class Type(enum.Enum): + Invalid, Url, Command = range(3) + + class TypeCondition(enum.Enum): + Page, Link, Image, Media, Text = range(5) + + id = "" + title = "" + menuTitle = "" + icon = QtGui.QIcon() + actionType = Type.Invalid + typeCondition = [ TypeCondition.Page, TypeCondition.Link, TypeCondition.Image, TypeCondition.Media ] + urlCondition = ".*" + submenu = "" + normalExec = "" + textExec = "" + supported = False + + def __init__(self, fileName): + data = Falkon.DesktopFile(fileName) + self.id = os.path.splitext(os.path.basename(fileName))[0] + self.title = data.name() + self.menuTitle = data.comment() + self.icon = QtGui.QIcon.fromTheme(data.icon(), QtGui.QIcon(os.path.join(os.path.dirname(fileName), data.icon()))) + self.actionType = Action.Type[data.value("X-RunAction-Type")] + self.typeCondition = list(map(lambda s: Action.TypeCondition[s], data.value("X-RunAction-TypeCondition").split(";"))) + self.urlCondition = data.value("X-RunAction-UrlCondition") or self.urlCondition + self.submenu = data.value("X-RunAction-Submenu") or self.submenu + self.normalExec = data.value("X-RunAction-Exec") or self.normalExec + self.textExec = data.value("X-RunAction-TextExec") or self.normalExec + self.supported = data.tryExec() + + def testAction(self, condition, url): + if not self.supported: return False + if not condition in self.typeCondition: return False + if not re.match(self.urlCondition, url.toString()): return False + return True + + def execAction(self, url, text=""): + url = str(url.toEncoded()) + if self.actionType == Action.Type.Command: + url = shlex.quote(url) + text = shlex.quote(text) + elif self.actionType == Action.Type.Url: + url = str(QtCore.QUrl.toPercentEncoding(url)) + text = str(QtCore.QUrl.toPercentEncoding(text)) + command = self.normalExec if text == "" else self.textExec + command = command.replace("{url}", url) + command = command.replace("{text}", text) + command = command.replace("{lang}", QtCore.QLocale.system().name()[:2]) + return command diff --git a/src/scripts/runaction/actionmanager.py b/src/scripts/runaction/actionmanager.py new file mode 100644 index 000000000..d1bd92bb5 --- /dev/null +++ b/src/scripts/runaction/actionmanager.py @@ -0,0 +1,113 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 . +# ============================================================ +import Falkon +import os, subprocess +from PySide2 import QtCore, QtGui, QtWidgets, QtUiTools +from runaction.action import Action +from runaction.settingsdialog import SettingsDialog + +class ActionManager(QtCore.QObject): + actions = [] + + def __init__(self, settingsPath, parent=None): + super().__init__(parent) + + self.settingsPath = settingsPath + settings = QtCore.QSettings(self.settingsPath + "/extensions.ini", QtCore.QSettings.IniFormat) + self._disabledActions = settings.value("RunAction/disabledActions") or [] + self.loadActions() + + def getActions(self, webView, r=None): + out = [] + menus = {} + + for action in list(filter(lambda a: a.id not in self.disabledActions, self.actions)): + url = webView.url() + text = "" + if r and webView.selectedText(): + cond = Action.TypeCondition.Text + text = webView.selectedText() + elif r and not r.linkUrl().isEmpty(): + cond = Action.TypeCondition.Link + url = r.linkUrl() + elif r and not r.imageUrl().isEmpty(): + cond = Action.TypeCondition.Image + url = r.imageUrl() + elif r and not r.mediaUrl().isEmpty(): + cond = Action.TypeCondition.Media + url = r.mediaUrl() + else: + cond = Action.TypeCondition.Page + + if action.testAction(cond, url): + act = Falkon.Action(action.icon, action.title, self) + act.triggered.connect(lambda a=action, w=webView, u=url, t=text: self.execAction(a, w, u, t)) + if action.submenu: + if not action.submenu in menus: + menu = Falkon.Menu(action.menuTitle, webView) + menus[action.submenu] = menu + out.append(menu) + menus[action.submenu].addAction(act) + else: + out.append(act) + + return out + + @property + def disabledActions(self): + return self._disabledActions + + @disabledActions.setter + def disabledActions(self, value): + settings = QtCore.QSettings(self.settingsPath + "/extensions.ini", QtCore.QSettings.IniFormat) + settings.setValue("RunAction/disabledActions", value) + self._disabledActions = value + + def showSettings(self, parent=None): + dialog = SettingsDialog(self, parent) + dialog.exec_() + + def execAction(self, action, webView, url, text=""): + command = action.execAction(url, text) + if action.actionType == Action.Type.Command: + subprocess.Popen(command, shell=True) + elif action.actionType == Action.Type.Url: + webView.openUrlInNewTab(QtCore.QUrl(command), Falkon.Qz.NT_SelectedTab) + + def loadActions(self): + self.actions = [] + + paths = [ + os.path.join(os.path.dirname(__file__), "actions"), + os.path.join(self.settingsPath, "runaction") + ] + + for path in paths: + if not os.path.exists(path): + continue + for file in os.listdir(path): + if not file.endswith(".desktop"): + continue + fileName = os.path.join(path, file) + try: + action = Action(fileName) + except Exception as e: + print("Failed to parse {}: {}".format(fileName, e)) + finally: + if action.supported: + self.actions.append(action) diff --git a/src/scripts/runaction/actions/dictionary.desktop b/src/scripts/runaction/actions/dictionary.desktop new file mode 100644 index 000000000..140b059f3 --- /dev/null +++ b/src/scripts/runaction/actions/dictionary.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Dictionary + +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Text" +X-RunAction-Exec="http://{lang}.wiktionary.org/wiki/Special:Search?search={text}" diff --git a/src/scripts/runaction/actions/google.png b/src/scripts/runaction/actions/google.png new file mode 100644 index 000000000..475d168dd Binary files /dev/null and b/src/scripts/runaction/actions/google.png differ diff --git a/src/scripts/runaction/actions/googleimagesearch.desktop b/src/scripts/runaction/actions/googleimagesearch.desktop new file mode 100644 index 000000000..94f166fd3 --- /dev/null +++ b/src/scripts/runaction/actions/googleimagesearch.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Google +Comment=Search with... + +Icon=google.png +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Image" +X-RunAction-Submenu="search_with" +X-RunAction-Exec="https://www.google.com/searchbyimage?site=search&image_url={url}" diff --git a/src/scripts/runaction/actions/mpv.desktop b/src/scripts/runaction/actions/mpv.desktop new file mode 100644 index 000000000..3c776bd63 --- /dev/null +++ b/src/scripts/runaction/actions/mpv.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=mpv + +Icon=mpv +TryExec=mpv +Type=Service + +X-RunAction-Type="Command" +X-RunAction-TypeCondition="Page;Link;Media" +X-RunAction-Exec="mpv {url}" diff --git a/src/scripts/runaction/actions/tineye.png b/src/scripts/runaction/actions/tineye.png new file mode 100644 index 000000000..29ab88aff Binary files /dev/null and b/src/scripts/runaction/actions/tineye.png differ diff --git a/src/scripts/runaction/actions/tineyeimagesearch.desktop b/src/scripts/runaction/actions/tineyeimagesearch.desktop new file mode 100644 index 000000000..10c1485a2 --- /dev/null +++ b/src/scripts/runaction/actions/tineyeimagesearch.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=TinEye +Comment=Search with... + +Icon=tineye.png +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Image" +X-RunAction-Submenu="search_with" +X-RunAction-Exec="http://www.tineye.com/search?url={url}" diff --git a/src/scripts/runaction/actions/translate.desktop b/src/scripts/runaction/actions/translate.desktop new file mode 100644 index 000000000..c23a1a2e2 --- /dev/null +++ b/src/scripts/runaction/actions/translate.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Translate page + +Icon=translate.png +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Page" +X-RunAction-Exec="http://translate.google.com/translate?sl=auto&tl={lang}&u={url}" diff --git a/src/scripts/runaction/actions/translate.png b/src/scripts/runaction/actions/translate.png new file mode 100644 index 000000000..4583aa782 Binary files /dev/null and b/src/scripts/runaction/actions/translate.png differ diff --git a/src/scripts/runaction/actions/w3.desktop b/src/scripts/runaction/actions/w3.desktop new file mode 100644 index 000000000..9855507e6 --- /dev/null +++ b/src/scripts/runaction/actions/w3.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Validate page + +Icon=w3.png +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Page" +X-RunAction-Exec="http://validator.w3.org/check?uri={url}" diff --git a/src/scripts/runaction/actions/w3.png b/src/scripts/runaction/actions/w3.png new file mode 100644 index 000000000..2fa751995 Binary files /dev/null and b/src/scripts/runaction/actions/w3.png differ diff --git a/src/scripts/runaction/actions/yandex.png b/src/scripts/runaction/actions/yandex.png new file mode 100644 index 000000000..101e7b600 Binary files /dev/null and b/src/scripts/runaction/actions/yandex.png differ diff --git a/src/scripts/runaction/actions/yandeximagesearch.desktop b/src/scripts/runaction/actions/yandeximagesearch.desktop new file mode 100644 index 000000000..60113521b --- /dev/null +++ b/src/scripts/runaction/actions/yandeximagesearch.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Yandex +Comment=Search with... + +Icon=yandex.png +Type=Service + +X-RunAction-Type="Url" +X-RunAction-TypeCondition="Image" +X-RunAction-Submenu="search_with" +X-RunAction-Exec="https://yandex.com/images/search?&img_url={url}&rpt=imageview" diff --git a/src/scripts/runaction/button.py b/src/scripts/runaction/button.py new file mode 100644 index 000000000..cd077dc8c --- /dev/null +++ b/src/scripts/runaction/button.py @@ -0,0 +1,50 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 . +# ============================================================ +import Falkon +import os +from PySide2 import QtGui, QtWidgets +from runaction.i18n import i18n + +class RunActionButton(Falkon.AbstractButtonInterface): + def __init__(self, manager): + super().__init__() + self.manager = manager + + self.setIcon(QtGui.QIcon(os.path.join(os.path.dirname(__file__), "icon.svg"))) + self.setTitle(i18n("Run Action")) + self.setToolTip(i18n("Run action on current page")) + + self.clicked.connect(self.onClicked) + + def id(self): + return "runaction-button" + + def name(self): + return i18n("RunAction button") + + def onClicked(self, controller): + self.menu = QtWidgets.QMenu() + + for action in self.manager.getActions(self.webView()): + self.menu.addAction(action) + + self.menu.addSeparator() + self.menu.addAction(QtGui.QIcon.fromTheme("configure"), i18n("Configure..."), self.manager.showSettings) + + self.menu.popup(controller.callPopupPosition(self.menu.sizeHint())) + self.menu.aboutToHide.connect(controller.callPopupClosed) diff --git a/src/scripts/runaction/icon.svg b/src/scripts/runaction/icon.svg new file mode 100644 index 000000000..9a1198916 --- /dev/null +++ b/src/scripts/runaction/icon.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/scripts/runaction/metadata.desktop b/src/scripts/runaction/metadata.desktop new file mode 100644 index 000000000..0024f9a82 --- /dev/null +++ b/src/scripts/runaction/metadata.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Run Action +Comment=Run various actions on sites + +Icon=icon.svg +Type=Service + +X-Falkon-Author=David Rosca +X-Falkon-Email=nowrep@gmail.com +X-Falkon-Version=0.1.0 +X-Falkon-Settings=true diff --git a/src/scripts/runaction/runaction.py b/src/scripts/runaction/runaction.py new file mode 100644 index 000000000..21819534c --- /dev/null +++ b/src/scripts/runaction/runaction.py @@ -0,0 +1,70 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 . +# ============================================================ +import Falkon +from PySide2 import QtCore +from runaction import actionmanager, button + +class RunActionPlugin(Falkon.PluginInterface, QtCore.QObject): + buttons = {} + manager = None + + def init(self, state, settingsPath): + plugins = Falkon.MainApplication.instance().plugins() + plugins.mainWindowCreated.connect(self.mainWindowCreated) + plugins.mainWindowDeleted.connect(self.mainWindowDeleted) + + self.manager = actionmanager.ActionManager(settingsPath) + + if state == Falkon.PluginInterface.LateInitState: + for window in Falkon.MainApplication.instance().windows(): + self.mainWindowCreated(window) + + def unload(self): + for window in Falkon.MainApplication.instance().windows(): + self.mainWindowDeleted(window) + + self.manager = None + + def testPlugin(self): + return True + + def populateWebViewMenu(self, menu, view, r): + for action in self.manager.getActions(view, r): + if action.inherits("QMenu"): + menu.addMenu(action).setParent(menu) + else: + action.setParent(menu) + menu.addAction(action) + + def showSettings(self, parent): + self.manager.showSettings(parent) + + def mainWindowCreated(self, window): + b = button.RunActionButton(self.manager) + window.statusBar().addButton(b) + window.navigationBar().addToolButton(b) + self.buttons[window] = b + + def mainWindowDeleted(self, window): + if not window in self.buttons: return + b = self.buttons[window] + window.statusBar().removeButton(b) + window.navigationBar().removeToolButton(b) + del self.buttons[window] + +Falkon.registerPlugin(RunActionPlugin()) diff --git a/src/scripts/runaction/settings.ui b/src/scripts/runaction/settings.ui new file mode 100644 index 000000000..9c2a108dc --- /dev/null +++ b/src/scripts/runaction/settings.ui @@ -0,0 +1,47 @@ + + + RunActionSettings + + + + 0 + 0 + 544 + 492 + + + + Dialog + + + + + + + + + + 16 + 16 + + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/scripts/runaction/settingsdialog.py b/src/scripts/runaction/settingsdialog.py new file mode 100644 index 000000000..5bb128207 --- /dev/null +++ b/src/scripts/runaction/settingsdialog.py @@ -0,0 +1,58 @@ +# ============================================================ +# RunAction plugin for Falkon +# Copyright (C) 2018 David Rosca +# +# 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 3 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 . +# ============================================================ +import Falkon +import os +from PySide2 import QtCore, QtGui, QtWidgets, QtUiTools +from runaction.i18n import i18n + +class SettingsDialog(QtWidgets.QDialog): + def __init__(self, manager, parent=None): + super().__init__(parent) + + self.manager = manager + + file = QtCore.QFile(os.path.join(os.path.dirname(__file__), "settings.ui")) + file.open(QtCore.QFile.ReadOnly) + self.ui = QtUiTools.QUiLoader().load(file, self) + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.ui) + + self.setMinimumSize(400, 250) + self.setWindowTitle(i18n("Run Action Settings")) + self.ui.label.setText("{}".format(i18n("Available actions"))) + + for action in self.manager.actions: + item = QtWidgets.QListWidgetItem(self.ui.listWidget) + item.setText(action.title) + item.setIcon(action.icon) + item.setData(QtCore.Qt.UserRole, action.id) + item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) + item.setCheckState(QtCore.Qt.Unchecked if action.id in self.manager.disabledActions else QtCore.Qt.Checked) + self.ui.listWidget.addItem(item) + + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + + def accept(self): + disabled = [] + for i in range(self.ui.listWidget.count()): + item = self.ui.listWidget.item(i) + if item.checkState() == QtCore.Qt.Unchecked: + disabled.append(item.data(QtCore.Qt.UserRole)) + self.manager.disabledActions = disabled + super().accept()