Files
epics_launcher/src/launcher.py
T
2020-02-25 03:06:31 -05:00

1286 lines
44 KiB
Python
Executable File

#!/usr/bin/env python
#
#
# 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.
import platform
import argparse
import copy
import enum
import shlex
import subprocess
import hashlib
import sys
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QDesktopServices, QIcon, QCursor, QKeySequence
from PyQt5.QtWidgets import QMainWindow, QMenu, QWidgetAction, QLineEdit, QWidget, QHBoxLayout, QToolButton, QVBoxLayout, QCheckBox, QAction, QLabel, QPushButton, QApplication, QInputDialog, QMessageBox
from .launcher_model import *
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
def stringContains(string, substring, caseSensitive):
if caseSensitive:
return substring in string
else:
return substring.lower() in string.lower()
def convertPwdToHash(password):
m = hashlib.md5()
m.update(password.encode())
return m.hexdigest()
def verifyPassword(object, password):
passwordInput = showPasswordDialog(object)
if passwordInput is not None:
convertedPwd = convertPwdToHash(passwordInput)
if convertedPwd == password:
return True
else:
showWrongPasswordDialog(object)
return False
def showPasswordDialog(object):
password, ok = QInputDialog.getText(object.window(),
'Password',
'Enter password:')
if ok:
return password
return None
def showWrongPasswordDialog(parent):
messageBox = QMessageBox(parent.window())
messageBox.setText("Wrong password")
messageBox.setStandardButtons(QMessageBox.Ok)
returnValue = messageBox.exec()
class SearchOptions(enum.Enum):
""" Enum with all search/filter options """
sensitivity = 0
text = 1
cmd = 2
class LauncherWindow(QMainWindow):
"""Launcher main window.
Main launcher window. At initialization recursively builds visualization
of launcher menus, builds menu bar, ...
"""
def __init__(self, rootFilePath, cfg, parent=None):
QMainWindow.__init__(self, parent)
# Get configuration for current system. platform.system() returns:
# - "Darwin" when OS X
# - "Linux" when Linux
# - "Windows" when Windows
systemType = platform.system()
if systemType == "Darwin":
systemType = "OS_X"
self.launcherCfg = cfg.get(systemType)
# From menu file define root directory (launcher_base)
path_tuple = os.path.split(rootFilePath)
self.launcherCfg["launcher_base"] = path_tuple[0]
rootFilePath = path_tuple[1]
# Get defined theme base. If it is not url or absolute dir, make it
# relative to config file.
theme_base = self.launcherCfg["theme_base"]
try:
urlopen(theme_base)
except (URLError, ValueError):
# Not an url. Check if absolute path.
if not os.path.isabs(theme_base):
self.launcherCfg["theme_base"] = os.path.join(cfg["cfg_base"],
theme_base)
self.menuModel = self.buildMenuModel(rootFilePath)
self.setWindowTitle(self.menuModel.main_title.text)
# QMainWindow has predefined layout. Content should be in the central
# widget. Create widget with a QVBoxLayout and set it as central.
mainWidget = QWidget(self)
self.mainLayout = QVBoxLayout(mainWidget)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(mainWidget)
# Main window consist of filter/search entry and a main button which
# pops up the root menu. Create a layout and add the items.
self.launcherMenu = LauncherSubMenu(self.menuModel, None, mainWidget)
self.mainButton = LauncherMainButton(self.launcherMenu, mainWidget)
self.launcherMenu.button = self.mainButton
# Create Filter/search item. Add it and main button to the layout.
self.use_sbox = self.menuModel.flags.get('search-box-enabled', True)
if self.use_sbox:
self.searchInput = LauncherFilterWidget(self.launcherMenu, mainWidget)
self.mainLayout.addWidget(self.searchInput)
self.mainLayout.addWidget(self.mainButton)
# Create menu bar. In current visualization menu bar exposes all
# LauncherFileChoiceItem items from the model. They are exposed in
# File menu.
menuBar = self.menuBar()
self.viewMenu = LauncherViewMenu("&View", menuBar)
self.viewMenu.buildViewMenu(self.menuModel)
menuBar.addMenu(self.viewMenu)
# Set mouse tracking
self.setMouseTracking(True)
mainWidget.setMouseTracking(True)
self.mainButton.setMouseTracking(True)
if self.use_sbox:
self.searchInput.setMouseTracking(True)
if self.menuModel.password is not None:
if not verifyPassword(self, self.menuModel.password):
sys.exit(-1)
def setNewView(self, rootMenuFile, text=None):
"""Rebuild launcher from new config file.
Destroy previous model and create new one. Build menus and edit main
window elements.
"""
self.menuModel.choice_element.text = self.windowTitle()
self.viewMenu.addToHistory(self.menuModel.choice_element)
del self.menuModel
self.menuModel = self.buildMenuModel(rootMenuFile)
if text:
self.setWindowTitle(text)
else:
self.setWindowTitle(self.menuModel.main_title.text)
self.mainButton.restyle(self.menuModel.main_title)
self.launcherMenu.deleteLater()
self.launcherMenu = LauncherSubMenu(self.menuModel, self.mainButton,
self)
self.mainButton.setMenu(self.launcherMenu)
self.viewMenu.buildViewMenu(self.menuModel)
self.use_sbox = self.menuModel.flags.get('search-box-enabled', True)
if self.use_sbox:
self.searchInput.setMenu(self.launcherMenu)
def changeEvent(self, changeEvent):
"""Catch when main window is selected and set focus to search."""
if changeEvent.type() == QtCore.QEvent.ActivationChange and \
self.isActiveWindow() and self.use_sbox:
self.searchInput.setFocus()
def mouseMoveEvent(self, event):
""" Activate window whenever mouse is over to get keyboard focus and show tooltips """
if not self.isActiveWindow():
self.activateWindow()
self.raise_() # Raise above other windows
def buildMenuModel(self, rootMenuPath):
"""Return model of a menu defined in rootMenuFile."""
rootMenuFullPath = join_launcher_path(self.launcherCfg.get("launcher_base"),
rootMenuPath)
self.launcherCfg["launcher_base"] = os.path.dirname(rootMenuFullPath)
try:
rootMenu = launcher_menu_model(None, rootMenuFullPath, 0, self.launcherCfg)
except IOError:
errMsg = "File \"" + rootMenuPath + "\" not found."
logging.error(errMsg)
sys.exit()
return rootMenu
class LauncherMenu(QMenu):
"""Super class of all menu visualizations.
Is a parent super class which takes the model of menu as an argument and
builds a "vital" part of the menu. It also implements methods for menu
manipulation.
"""
def __init__(self, menuModel, button=None, parent=None):
QMenu.__init__(self, parent)
self.setSeparatorsCollapsible(False)
self.filterTerm = ""
self.menuModel = menuModel
self.buildMenu(self.menuModel.menu_items)
self.initFilterVisibility = True
self.filterConditions = [False, True, False]
self.button = button
def buildMenu(self, menuModel):
"""Visualize menu
menuModel has a list of menu_items with models of items. Build buttons
from it and add them to the menu.
"""
sectionTitle = None
for item in self.menuModel.menu_items:
if item.__class__.__name__ == "launcher_cmd_item":
self.appendToMenu(LauncherCmdButton(item, sectionTitle, self))
elif item.__class__.__name__ == "launcher_sub_menu_item":
self.appendToMenu(LauncherMenuButton(item, sectionTitle, self))
elif item.__class__.__name__ == "launcher_title_item":
sectionTitle = None
titleButton = LauncherMenuTitle(item, sectionTitle, self)
self.appendToMenu(titleButton)
sectionTitle = titleButton
elif item.__class__.__name__ == "launcher_item_separator":
self.addAction(LauncherSeparator(item, self))
def appendToMenu(self, widget):
"""Append action to menu.
Create widget action for widget. Pair them and add to the menu.
"""
self.action = LauncherMenuWidgetAction(widget, self)
self.addAction(self.action)
def insertToMenu(self, widget, index):
"""Insert action to specified position in menu.
Create widget action for widget. Pair them and insert them to the
specified position in menu.
"""
self.action = LauncherMenuWidgetAction(widget, self)
if (len(self.actions()) > index):
self.insertAction(self.actions()[index], self.action)
else:
self.addAction(self.action)
def setFilterCondition(self, condition, value):
""" Set one condition to given value."""
self.filterConditions[condition.value] = value
self.filterMenu(self.filterTerm)
def getLauncherWindow(self):
""" Search and return application main window object"""
candidate = self
while type(candidate) != LauncherWindow:
candidate = candidate.parent()
print(candidate)
return candidate
def filterMenu(self, filterTerm=None):
"""Filter menu items with filterTerm
Shows/hides menu items depending on filterTerm. Returns true if has
visible active (buttons) items.
"""
self.filterTerm = filterTerm
hasVisible = False
# Read filters
sensitivityFilter = self.filterConditions[
SearchOptions.sensitivity.value]
textFilter = self.filterConditions[SearchOptions.text.value]
cmdFilter = self.filterConditions[SearchOptions.cmd.value]
# Skip first item since it is either search entry or detach button.
for action in self.actions()[1:len(self.actions())]:
if action.__class__.__name__ == "LauncherMenuWidgetAction":
widget = action.defaultWidget()
# Search is always only on NamedButtons. Get text from their
# models to avoid searching also the prefixes added later (in
# search view).
if isinstance(widget, LauncherNamedButton):
text = widget.itemModel.text
widgetType = widget.__class__.__name__
else:
widget = None
widgetType = None
text = ""
if action.__class__.__name__ == "LauncherSeparator":
action.setVisibility(not filterTerm and self.initFilterVisibility)
elif not filterTerm:
# Empty filter. Show depending on type. If submenu recursively
# empty filter.
action.setVisibility(self.initFilterVisibility)
if widgetType == "LauncherMenuButton":
widget.menu().filterMenu(filterTerm)
elif widgetType == "LauncherMenuTitle":
action.setVisibility(False)
elif widgetType == "LauncherSubMenuAsTitle":
action.setVisibility(False)
elif widgetType == "LauncherMenuButton":
# Recursively filter menus. Show only sub-menus that have
# visible items.
subMenu = widget.menu()
subHasVisible = subMenu.filterMenu(filterTerm)
hasVisible = hasVisible or subHasVisible
action.setVisibility(subHasVisible)
elif textFilter and stringContains(text, filterTerm,
sensitivityFilter):
action.setVisibility(True)
hasVisible = True
elif widgetType == "LauncherCmdButton" and cmdFilter and\
stringContains(widget.cmd, filterTerm, sensitivityFilter):
action.setVisibility(True)
hasVisible = True
else:
action.setVisibility(False)
return hasVisible
def showEvent(self, showEvent):
"""Catch event when menu is shown and move it by side.
Whenever show(), popup(), exec() are called this method is called.
Move the menu to the left side of the button (default is bellow)
"""
# TODO handle cases when to close to the edge of screen.
# Do nothing if the event comes from outside the application
if not showEvent.spontaneous():
if self.button:
width = self.button.width()
height = self.button.height()
else:
width = self.parent().width()
height = self.parent().height()
position = self.pos()
position.setX(position.x()+width)
position.setY(position.y()-height)
self.move(position)
# Set focus on first button (skip separators (also titles) and detach
# button) If empty menu focus on detach button
i = 1
try:
while self.actions()[i].isSeparator():
i += 1
except LookupError:
i = 0
self.actions()[i].defaultWidget().setFocus()
self.setActiveAction(self.actions()[i])
def getMainMenu(self):
"""Return menu of mainButton from which all menus expand.
All LauncherMenu menus visualized from the same root menu model
have lowest common ancestor which is a menu of mainButton. If this menu
is destroyed all other are also destroyed, and all detached menus are
closed. Because each Qt element holds reference to its parent, this
menu can be recursively determined.
"""
return self.getLauncherWindow().mainButton.menu()
class LauncherSubMenu(LauncherMenu):
"""Visualization of sub-menu.
Implements a visualization of the menu when used as a sub menu. Popuped
from the main menu or button.
Creates detach button and adds it to the menu.
"""
def __init__(self, menuModel, button, parent=None):
LauncherMenu.__init__(self, menuModel, button, parent)
self.detachButton = LauncherDetachButton(self)
self.insertToMenu(self.detachButton, 0)
def detach(self):
"""Open menu in new window.
Creates new menu and opens it as new window. Menu parent should be
mainButton on main window. This way it will be closed only if the
launcher is close or the root menu is changed.
"""
detachedMenu = LauncherDetachedMenu(self.menuModel,
self.getMainMenu())
detachedMenu.setWindowTitle(self.menuModel.main_title.text)
detachedMenu.searchInput.setText(self.filterTerm)
detachedMenu.show()
# Takes care of showing on the right place with right size
detachedMenu.popup(QtCore.QPoint(self.pos().x(), self.pos().y()))
self.hideAll()
def hideAll(self):
""" Recursively hide all popuped menus"""
self.hide()
candidate = self.parent()
if isinstance(candidate, LauncherSubMenu):
candidate.hideAll()
class LauncherDetachedMenu(LauncherMenu):
"""Visualization of detached menu.
Creates detached menu. Adds find/search input to menu. Also overrides the
hide() method of the menu, because it should not be hidden when action
is performed.
Removes detach button and filter/search widget but do not add them to
the menu.
"""
def __init__(self, menuModel, parent=None):
LauncherMenu.__init__(self, menuModel, None, parent)
self.searchInput = LauncherFilterWidget(self, self)
self.insertToMenu(self.searchInput, 0)
self.setWindowFlags(Qt.Window)
self.setAttribute(Qt.WA_X11NetWmWindowTypeMenu, True)
self.setEnabled(True)
def hide(self):
pass # Detached menu should not be hidden at any action (left key).
def hideAll(self):
pass
def changeEvent(self, changeEvent):
"""Catch when menu window is selected and focus to search."""
if changeEvent.type() == QtCore.QEvent.ActivationChange and \
self.isActiveWindow():
self.searchInput.setFocus()
def keyPressEvent(self, event):
"""Catch escape (originally closes the window) and skip actions."""
if event.key() == Qt.Key_Escape or event.key() == Qt.Key_Alt:
pass
else:
LauncherMenu.keyPressEvent(self, event)
def mouseMoveEvent(self, event):
# Activate window whenever mouse is over to get keyboard focus and show tooltips
if not self.isActiveWindow():
self.activateWindow()
self.raise_() # Raise above other windows
class LauncherSearchMenuView(LauncherMenu):
"""Search view
Different visualization of launcher for searching. Submenues do not
expand, but are rather included at the bottom of the list.
"""
def __init__(self, menuModel, button=None, parent=None):
LauncherMenu.__init__(self, menuModel, button, parent)
self.searchWidget = LauncherSearchWidget(self, self.getMainMenu())
self.insertToMenu(self.searchWidget, 0)
self.initFilterVisibility = False
def buildMenu(self, menuModel):
"""Visualize menu
Override this method and build different visualization.
"""
cMenuItems = list(self.menuModel.menu_items)
level = 0
sectionTitle = None
for item in cMenuItems:
levelPrefix = ""
addPrefix = False
for traceItem in item.trace:
levelPrefix = levelPrefix + traceItem.text + " > "
if item.__class__.__name__ == "launcher_cmd_item":
button = LauncherCmdButton(item, sectionTitle, self)
self.appendToMenu(button)
addPrefix = True
elif item.__class__.__name__ == "launcher_sub_menu_item":
# Take subemnu model and build (visualize) it below
cSubMenuItems = copy.copy(item.sub_menu.menu_items)
cMenuItems.extend(cSubMenuItems)
elif item.__class__.__name__ == "launcher_title_item":
button = LauncherMenuTitle(item, None, self)
self.appendToMenu(button)
sectionTitle = button
addPrefix = True
elif item.__class__.__name__ == "launcher_item_separator":
self.addAction(LauncherSeparator(item, self))
if addPrefix: # Add level prefix
button.setText(levelPrefix + button.text())
def exposeMenu(self, searchInput=None):
"""Open menu in new window.
Creates new menu and opens it as new window. Menu parent should be
mainButton on launcherWindow. This way it will be closed only if the
launcher is close or the root menu is changed.
"""
self.setWindowTitle("Search")
self.searchWidget.setText(searchInput)
self.filterMenu(searchInput)
self.setWindowFlags(Qt.Window | Qt.Tool)
self.setAttribute(Qt.WA_X11NetWmWindowTypeMenu, True)
self.setEnabled(True)
self.show()
self.searchWidget.setFocus()
#self.move(self.pos().x(), self.pos().y()) TODO
def changeEvent(self, changeEvent):
"""Catch when menu window is selected and focus to search."""
if changeEvent.type() == QtCore.QEvent.ActivationChange and \
self.isActiveWindow():
self.searchWidget.setFocus()
def hide(self):
pass # Search menu should not be hidden at any action (left key).
def hideAll(self):
pass
class LauncherMenuWidgetAction(QWidgetAction):
"""Wrap widgets to be added to menu.
When QWidget needs to be appended to QMenu it must be "wrapped" into
QWidgetAction. This class "wraps" LauncherButton in the same way.
Further it also implements method to control the visibility of the menu
item (both action and widget).
"""
def __init__(self, widget, parent=None):
QWidgetAction.__init__(self, parent)
self.widget = widget
self.setDefaultWidget(self.widget)
widget.setMyAction(self) # Let widget know about action.
def setVisibility(self, visibility):
"""Set visibility of both the widget action and the widget."""
self.setVisible(visibility)
self.widget.setVisible(visibility)
if self.widget.sectionTitle and visibility:
self.widget.sectionTitle.myAction.setVisibility(True)
class LauncherFilterLineEdit(QLineEdit):
"""Input field with an option to clear it.
LauncherFilterLineEdit is QLineEdit which does filtering of menu items
recursively by putting the filter to child menus. It has a button to clear
current input with one click. When enter button is pressed a search window
with results is opened.
"""
def __init__(self, menu, parent=None):
QLineEdit.__init__(self, parent)
self.menu = menu
self.textChanged.connect(lambda: self.menu.filterMenu(self.text()))
self.myAction = None
self.setPlaceholderText("Enter filter term.")
self.setClearButtonEnabled(True)
# Create button to clear text and add it to the right edge of the
# input.
self.setMinimumWidth(200)
# Set search policy (default False). If True it opens search when
# Enter is pressed
self.searchPolicy = False
def setMyAction(self, action):
self.myAction = action
def keyPressEvent(self, event):
"""Catch key pressed event.
Catch return and enter key pressed and open search in new window.
"""
if self.searchPolicy is True and \
((event.key() == Qt.Key_Return) or
(event.key() == Qt.Key_Enter)):
self.openSearch()
elif event.key() == Qt.Key_Down:
self.focusNextPrevChild(True)
elif event.key() == Qt.Key_Up:
self.focusNextPrevChild(False)
else:
QLineEdit.keyPressEvent(self, event)
def openSearch(self):
""" Do a search on full menu (root menu)."""
menu = self.menu.getMainMenu()
searchMenu = LauncherSearchMenuView(menu.menuModel, menu.button,
menu)
searchMenu.exposeMenu(self.text())
def mouseMoveEvent(self, event):
self.parent().mouseMoveEvent(event)
class LauncherFilterWidget(QWidget):
""" Filter menu widget which opens search when return is pressed"""
def __init__(self, menu, parent=None):
QWidget.__init__(self, parent)
mainLayout = QHBoxLayout(self)
self.setLayout(mainLayout)
self.searchInput = LauncherFilterLineEdit(menu, self)
self.searchInput.searchPolicy = True
self.searchButton = QToolButton(self)
self.searchButton.setMouseTracking(True)
self.searchButton.setFixedSize(27, 27)
currDir = os.path.dirname(os.path.realpath(__file__))
icon = QIcon(os.path.join(currDir,
"resources/images/magnifying-glass-2x.png"))
self.searchButton.setIcon(icon)
self.searchButton.setFocusPolicy(Qt.ClickFocus)
self.searchButton.clicked.connect(
lambda: self.searchInput.openSearch())
mainLayout.addWidget(self.searchInput)
mainLayout.addWidget(self.searchButton)
# Focus policy: get focus when tabing/arrow key and pass it to
# search input
self.setFocusPolicy(Qt.TabFocus)
self.setFocusProxy(self.searchInput)
def setMenu(self, menu):
self.searchInput.menu = menu
def setMyAction(self, action):
self.myAction = action
def setText(self, text):
self.searchInput.setText(text)
def mouseMoveEvent(self, event):
self.parent().mouseMoveEvent(event)
class LauncherSearchWidget(QWidget):
""" Search menu widget
Container with all visual elements to do a search. Some options are
available as check boxes.
"""
def __init__(self, menu, parent=None):
QWidget.__init__(self, parent)
mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(0,0,0,0)
self.setLayout(mainLayout)
# Prepare components and add them to layout:
# - search field
# - options (case sensitive)
# - filters (text, cmd)
# - menu
self.searchInput = LauncherFilterLineEdit(menu, self)
caseSensitive = QCheckBox("Case sensitive", self)
caseSensitive.setChecked(False)
caseSensitive.stateChanged.connect(lambda: menu.setFilterCondition(
SearchOptions.sensitivity, caseSensitive.isChecked()))
menu.setFilterCondition(SearchOptions.sensitivity,
caseSensitive.isChecked())
mainLayout.addWidget(self.searchInput)
mainLayout.addWidget(caseSensitive)
options = QWidget(self)
optionsLayout = QHBoxLayout(options)
optionsLayout.setContentsMargins(0, 0, 0, 0)
searchText = QCheckBox("Title search", options)
searchText.setChecked(True)
searchText.stateChanged.connect(
lambda: menu.setFilterCondition(SearchOptions.text,
searchText.isChecked()))
menu.setFilterCondition(SearchOptions.text, searchText.isChecked())
searchCmd = QCheckBox("Command search", options)
searchCmd.setChecked(False)
searchCmd.stateChanged.connect(
lambda: menu.setFilterCondition(SearchOptions.cmd,
searchCmd.isChecked()))
menu.setFilterCondition(SearchOptions.cmd, searchCmd.isChecked())
optionsLayout.addWidget(searchText)
optionsLayout.addWidget(searchCmd)
options.setLayout(optionsLayout)
mainLayout.addWidget(options)
self.searchInput.setPlaceholderText("Enter search term.")
self.myAction = None
# Focus policy: get focus when tabing/arrow key and pass it to
# search input
self.setFocusPolicy(Qt.TabFocus)
self.setFocusProxy(self.searchInput)
def setText(self, text):
self.searchInput.setText(text)
def setMyAction(self, action):
self.myAction = action
class LauncherSeparator(QAction):
"""Just wrapped normal menu separator."""
def __init__(self, itemModel, parent=None):
QAction.__init__(self, parent)
self.setSeparator(True)
def setVisibility(self, visibility):
self.setVisible(visibility)
class LauncherMenuTitle(QLabel):
"""Passive element with no action and no key focus."""
def __init__(self, itemModel, sectionTitle=None, parent=None):
QLabel.__init__(self, itemModel.text, parent)
self.myAction = None
self.setFocusPolicy(Qt.NoFocus)
# For title element sectionTitle is menu button that owns menu with
# this element.
self.sectionTitle = sectionTitle
# Apply custom styles
style = LauncherStyle(self, itemModel.theme, itemModel.style)
self.setStyleSheet(style.style)
def setMyAction(self, action):
self.myAction = action
self.myAction.setSeparator(True)
class LauncherButton(QPushButton):
"""Super class for active menu items (buttons).
Parent class for all active menu items. To recreate the native menu
behavior (when QActions are used) this class also handles keyboard
events to navigate through the menu.
Parent of any LauncherButton must be a QMenu and it must be paired with
a LauncherMenuWidgetAction.
"""
def __init__(self, sectionTitle=None, parent=None):
QPushButton.__init__(self, parent)
self.setMouseTracking(True)
self.myAction = None
self.sectionTitle = sectionTitle
self.contextMenu = QMenu(self)
def contextMenuEvent(self, event):
""" Show context menu if context exists"""
if self.contextMenu.actions():
self.contextMenu.exec_(QCursor.pos())
def mousePressEvent(self, event):
""" Catch right menu clicks, to override closing of menu """
if event.button() == Qt.RightButton:
pass # contexMenuEvent is still emitted and handled
else:
super(QPushButton, self).mousePressEvent(event)
def setMyAction(self, action):
self.myAction = action
def keyPressEvent(self, event):
"""Catch key pressed event.
Catch return and enter key pressed. Send clicked command to execute
action.
Catch left arrow button pressed on any of the menu buttons, to hide
the whole menu (parent) and return to previous level menu.
Catch right arrow button and skip it.
"""
if (event.key() == Qt.Key_Return) or (event.key() == Qt.Key_Enter):
self.click()
elif event.key() == Qt.Key_Left:
self.parent().hide()
self.parent().button.activate()
elif event.key() == Qt.Key_Right:
pass
elif event.key() == Qt.Key_Down:
self.focusNextPrevChild(True)
elif event.key() == Qt.Key_Up:
self.focusNextPrevChild(False)
else:
QPushButton.keyPressEvent(self, event)
def mouseMoveEvent(self, event):
self.activate()
self.parent().mouseMoveEvent(event)
def activate(self):
self.setFocus()
if isinstance(self.parent(), QMenu):
# only menus have actions
self.parent().setActiveAction(self.myAction)
class LauncherDetachButton(LauncherButton):
"""Button to detach menu and show it in new window.
LauncherDetachButton is always shown as first item of popup menu. It
builds new LauncherMenu from the model of parent menu and opens it in
a new window.
"""
def __init__(self, parent=None):
LauncherButton.__init__(self, None, parent)
self.clicked.connect(parent.detach)
class LauncherMainButton(LauncherButton):
"""Main Launcher button to expand menu
This class extends LauncherButton similar as LauncherMenuButton, but
with a different inputs.
"""
def __init__(self, menu, parent=None):
LauncherButton.__init__(self, None, parent)
self.restyle(menu.menuModel.main_title)
self.setMenu(menu)
def restyle(self, itemModel):
self.setText(itemModel.text.replace('&', '&&')) # For QButton &X means that X is shortcut, && gives &
style = LauncherStyle(self, itemModel.theme, itemModel.style)
# Add menu arrow indicator. Added here to use right path and avoid
# compiling python code
currDir = os.path.dirname(os.path.realpath(__file__))
indicator = os.path.join(currDir, "resources/images/caret-right.png")
indicator = os.path.normpath(indicator)
# Even on windows a path to the image must be with forward slashes.
indicator = re.sub(r'\\', '/', indicator)
indicatorStyle = "LauncherButton:menu-indicator {image: url(" +\
indicator + ");subcontrol-position: right center}"
style.appendClassStyle(indicatorStyle)
self.setStyleSheet(style.style)
def mouseMoveEvent(self, event):
self.parent().mouseMoveEvent(event)
self.setFocus()
def keyPressEvent(self, event):
"""Open main menu
Open menu also with right arrow key. Override option to hide parent
with left arrow key, because main button is not part of popuped window.
"""
if event.key() == Qt.Key_Right:
self.setFocus()
self.click()
elif event.key() == Qt.Key_Left:
pass
else:
LauncherButton.keyPressEvent(self, event)
class LauncherNamedButton(LauncherButton):
"""Parent class to menu all buttons with text."""
def __init__(self, itemModel, sectionTitle=None, parent=None):
LauncherButton.__init__(self, sectionTitle, parent)
self.setText(itemModel.text.replace('&', '&&')) # For QButton &X means that X is shortcut, && gives &
style = LauncherStyle(self, itemModel.theme, itemModel.style)
# Add menu arrow indicator. Added here to use right path and avoid
# compiling python code
currDir = os.path.dirname(os.path.realpath(__file__))
indicator = os.path.join(currDir, "resources/images/caret-right.png")
indicator = os.path.normpath(indicator)
# Even on windows a path to the image must be with forward slashes.
indicator = re.sub(r'\\', '/', indicator)
indicatorStyle = "LauncherButton:menu-indicator {image: url(" +\
indicator + ");subcontrol-position: right center}"
style.appendClassStyle(indicatorStyle)
self.setStyleSheet(style.style)
if itemModel.help_link:
helpAction = QAction("&Help", self)
helpAction.setData(itemModel.help_link)
self.contextMenu.addAction(helpAction)
helpAction.triggered.connect(self.openHelp)
self.itemModel = itemModel
def openHelp(self):
""" Open help link in default browser. """
url = QtCore.QUrl(
self.sender().data(), QtCore.QUrl.TolerantMode)
QDesktopServices.openUrl(url)
class LauncherCmdButton(LauncherNamedButton):
""" LauncherCmdButton executes shell command. """
def __init__(self, itemModel, sectionTitle=None, parent=None):
LauncherNamedButton.__init__(self, itemModel, sectionTitle, parent)
self.cmd = itemModel.cmd
self.pwd = itemModel.pwd
self.clicked.connect(self.executeCmd)
toolTip = ""
if itemModel.tip:
toolTip = itemModel.tip + " "
toolTip = toolTip + "[Command: " + self.cmd + "]"
self.setToolTip(toolTip)
copyAction = QAction("Copy command", self)
copyAction.triggered.connect(self.copyCmd)
# If actions (Help) already exist put above them
if self.contextMenu.actions():
self.contextMenu.insertAction(self.contextMenu.actions()[0], copyAction)
else:
self.contextMenu.addAction(copyAction)
def copyCmd(self):
cb = QApplication.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(self.cmd, mode=cb.Clipboard)
def executeCmd(self, itemModel):
""" Run specified command as a separate process
Runs commands from the same environment ($PATH) as the launcher was
started. Apart from "bash" it aboarts scripts without shebang on
first line (strictly).
"""
self.parent().hideAll() # When done hide all popuped menus
try:
if self.pwd is not None:
"""passwordInput = showPasswordDialog(self)
if passwordInput is not None:
convertedPwd = convertPwdToHash(passwordInput)
if convertedPwd != self.pwd:
showWrongPasswordDialog()
return"""
if verifyPassword(self, self.pwd):
subprocess.Popen(shlex.split(self.cmd))
except OSError:
warn_msg = "Command \"" + self.cmd + "\" cannot be executed. " + \
"Wrong path or bad/no interpreter."
logging.warning(warn_msg)
class LauncherMenuButton(LauncherNamedButton):
"""Builds a new view from model.
LauncherMenuButton builds new menu from model. When pressed the menu is
popped up.
"""
def __init__(self, itemModel, sectionTitle=None, parent=None):
LauncherNamedButton.__init__(self, itemModel, sectionTitle, parent)
menu = LauncherSubMenu(itemModel.sub_menu, self, self.parent())
self.setMenu(menu)
toolTip = ""
if itemModel.tip:
toolTip = itemModel.tip + " "
toolTip = toolTip + "[Menu: " + menu.menuModel.main_title.text + "]"
self.setToolTip(toolTip)
def keyPressEvent(self, event):
"""Submenu can also be opened with right arrow key."""
if event.key() == Qt.Key_Right:
self.click()
else:
LauncherNamedButton.keyPressEvent(self, event)
class LauncherViewMenu(QMenu):
""" View menu for menu bar """
def __init__(self, text, parent=None):
QMenu.__init__(self, text, parent)
self.historyMenu = QMenu("History", self)
self.initHistoryMenu()
self.maxHistoryLength = 10
# When creating menu first time (for the main root menu), a special
# menu entry should be created, which will allow user to alwayas return
# to that very first/main menu. It should always be the top choice in the
# View menu, except when main menu is already loaded.
self.rootMenuElement = None
def buildViewMenu(self, menuModel):
self.menuModel = menuModel
if not self.rootMenuElement:
# This executes only first time
self.rootMenuElement = self.menuModel.choice_element
self.clear()
# Option to go to the root menu if not already at it.
if not self.menuModel.choice_element.root_menu_file == self.rootMenuElement.root_menu_file:
action = LauncherFileChoiceAction(self.rootMenuElement, self)
self.addAction(action)
self.addSeparator()
for view in menuModel.file_choices:
action = LauncherFileChoiceAction(view, self)
self.addAction(action)
self.addSeparator()
self.addMenu(self.historyMenu)
self.addSeparator()
searchAction = QAction("Search", self)
searchAction.setShortcuts(QKeySequence("Ctrl+F"))
searchAction.setStatusTip("Search launcher items")
searchAction.triggered.connect(self.openSearch)
self.addAction(searchAction)
def initHistoryMenu(self):
self.historyMenu.clear()
self.historyMenu.addSeparator()
clearHistory = self.historyMenu.addAction("Clear history")
clearHistory.triggered.connect(self.initHistoryMenu)
self.historyMenu.menuAction().setVisible(False)
def addToHistory(self, itemModel):
action = LauncherFileChoiceAction(itemModel, self)
history_list = self.historyMenu.actions()
if len(history_list)-2 >= self.maxHistoryLength:
# Remove first action which is not separator or clear action
self.historyMenu.removeAction(history_list[-3])
self.historyMenu.insertAction(self.historyMenu.actions()[0], action)
self.historyMenu.menuAction().setVisible(True)
def openSearch(self):
searchMenu = LauncherSearchMenuView(self.menuModel,
self.parent().parent().mainButton,
self.parent().parent().launcherMenu)
searchMenu.exposeMenu("")
class LauncherFileChoiceAction(QAction):
"""Action to change the root menu of the launcher.
LauncherFileChoiceAction causes the launcher to change the root menu and
sets new view. It is placed in a View menu.
"""
def __init__(self, itemModel, parent=None):
# For QAction &X means that X is shortcut, && gives &
QAction.__init__(self, itemModel.text.replace('&', '&&'), parent)
self.itemModel = itemModel
self.triggered.connect(self.changeView)
def changeView(self):
"""Find LauncherWindow and set new view."""
candidate = self
while candidate.__class__.__name__ != "LauncherWindow":
candidate = candidate.parent()
candidate.setNewView(self.itemModel.root_menu_file, self.itemModel.text)
class LauncherStyle(object):
""" Class which handles qss style sheet from multiple sources """
def __init__(self, item, theme=None, style=None):
self.item = item
self.styleString = ""
self.style = self.styleString
if theme:
self.appendThemeStyle(theme)
if style and item:
self.appendStyle(style, item)
def appendThemeStyle(self, theme):
mainWindow = self.item
while type(mainWindow) is not LauncherWindow:
mainWindow = mainWindow.parent()
try:
theme_file = open_launcher_file(
os.path.join(mainWindow.launcherCfg.get("theme_base"),
theme + ".qss"))
self.styleString = self.styleString + theme_file.read().decode('utf-8')
theme_file.close()
self.style = self.styleString
except IOError:
warnMsg = "Theme \"" + theme + \
"\" was not found. Theme ignored."
logging.warning(warnMsg)
def appendStyle(self, style, item):
self.styleString = self.styleString + item.__class__.__name__ +\
"{" + style + "}"
self.style = self.styleString
def appendClassStyle(self, style):
self.styleString = self.styleString + style
self.style = self.styleString
def main():
""" Main logic """
argsPars = argparse.ArgumentParser()
argsPars.add_argument('configuration',
help="menu/configuration file")
argsPars.add_argument('-m', '--mapping',
help='overwrite default mapping file')
argsPars.add_argument('-s', '--style',
help="overwrite default style (qss file)")
argsPars.add_argument('--position', type=int, nargs=2, metavar=('X', 'Y'),
help="set initial position on the screen")
args = argsPars.parse_args()
app = QApplication(sys.argv)
# Load configuration. Use default configuration defined inside package if
# --config is not specified
currDir = os.path.dirname(os.path.realpath(__file__))
cfgPath = os.path.join(currDir, "resources/mapping/mapping.json")
cfgFile = open_launcher_file(cfgPath)
cfgString = cfgFile.read(-1).decode('utf-8')
defaultCfg = json.loads(cfgString)
defaultCfg["cfg_base"] = os.path.dirname(cfgPath)
cfgFile.close()
default = True
logMsg = ""
if args.mapping:
try:
cfgFile = open_launcher_file(args.mapping)
cfgString = cfgFile.read(-1).decode('utf-8')
cfg = json.loads(cfgString)
cfg["cfg_base"] = os.path.dirname(args.mapping)
cfgFile.close()
default = False
except:
logMsg = "Problems opening \"" + args.mapping + "\". "
if default:
cfg = defaultCfg
logMsg += "Launcher will be loaded with default mapping."
logging.warning(logMsg)
# Create Launcher Window and load default style and theme
launcherWindow = LauncherWindow(args.configuration, cfg)
app.setStyle("cleanlooks")
styleFile = open_launcher_file(os.path.join(currDir,
"resources/qss/default.qss"))
app.setStyleSheet(styleFile.read().decode('utf-8'))
styleFile.close()
if args.style:
try:
userStyle = open_launcher_file(args.style)
launcherWindow.setStyleSheet(userStyle.read().decode('utf-8'))
userStyle.close()
except:
LogMsg = "Problems opening \"" + args.style + "\". " + \
"Launcher will be opened with default style."
logging.warning(logMsg)
launcherWindow.setMinimumWidth(250)
launcherWindow.show()
geometry = launcherWindow.geometry()
# Set to desired position
position = args.position
if not position: # Set defaults
position = [0, 0]
screenGeometry = app.desktop().geometry()
# Negative values should be treated as starting from oposite corner
if position[0] < 0: # X
position[0] = screenGeometry.width()-geometry.width()+position[0]
if position[1] < 0: # Y
position[1] = screenGeometry.height()-geometry.height()+position[1]
# Update x/y coordinates of window
launcherWindow.move(position[0], position[1])
sys.exit(app.exec_())
# Start program here
if __name__ == '__main__':
main()