Merge remote-tracking branch 'origin/pr/258'
* origin/pr/258: qvm-template-gui: auto resize columns to the content qvm-template-gui: change date format to '%d %b %Y' qvm-template-gui: UX improvements Make pylint happy qvm-template-gui: improve displaying progress qvm-template-gui: UI improvements qvm-template: Include files in deb/rpm package qvm-template: Change size unit from kB to MB. qvm-template: Disable multi-selection for simplicity. qvm-template: Port initial code to PyQt. qvm-template: Initial GUI implementation.
This commit is contained in:
commit
b2c40eb578
5
debian/install
vendored
5
debian/install
vendored
@ -7,6 +7,7 @@
|
||||
/usr/bin/qubes-qube-manager
|
||||
/usr/bin/qubes-log-viewer
|
||||
/usr/bin/qubes-template-manager
|
||||
/usr/bin/qvm-template-gui
|
||||
/usr/bin/qubes-vm-clone
|
||||
/usr/libexec/qubes-manager/mount_for_backup.sh
|
||||
/usr/libexec/qubes-manager/qvm_about.sh
|
||||
@ -34,6 +35,7 @@
|
||||
/usr/lib/*/dist-packages/qubesmanager/bootfromdevice.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/device_list.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/template_manager.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/qvm_template_gui.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/clone_vm.py
|
||||
|
||||
/usr/lib/*/dist-packages/qubesmanager/resources_rc.py
|
||||
@ -54,6 +56,9 @@
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_devicelist.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_templatemanager.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_clonevmdlg.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_qvmtemplate.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_templateinstallconfirmdlg.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/ui_templateinstallprogressdlg.py
|
||||
/usr/lib/*/dist-packages/qubesmanager/i18n/qubesmanager_*.qm
|
||||
/usr/lib/*/dist-packages/qubesmanager/i18n/qubesmanager_*.ts
|
||||
|
||||
|
446
qubesmanager/qvm_template_gui.py
Normal file
446
qubesmanager/qvm_template_gui.py
Normal file
@ -0,0 +1,446 @@
|
||||
import asyncio
|
||||
import collections
|
||||
from datetime import datetime
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import typing
|
||||
|
||||
import PyQt5 # pylint: disable=import-error
|
||||
import PyQt5.QtWidgets # pylint: disable=import-error
|
||||
|
||||
from . import ui_qvmtemplate # pylint: disable=no-name-in-module
|
||||
from . import ui_templateinstallconfirmdlg # pylint: disable=no-name-in-module
|
||||
from . import ui_templateinstallprogressdlg # pylint: disable=no-name-in-module
|
||||
from . import utils
|
||||
|
||||
#pylint: disable=invalid-name
|
||||
|
||||
BASE_CMD = ['qvm-template', '--enablerepo=*', '--yes']
|
||||
|
||||
# singleton for "no date"
|
||||
ZERO_DATE = datetime.utcfromtimestamp(0)
|
||||
|
||||
# pylint: disable=too-few-public-methods,inherit-non-class
|
||||
class Template(typing.NamedTuple):
|
||||
status: str
|
||||
name: str
|
||||
evr: str
|
||||
reponame: str
|
||||
size: int
|
||||
buildtime: datetime
|
||||
installtime: typing.Optional[datetime]
|
||||
#licence: str
|
||||
#url: str
|
||||
#summary: str
|
||||
# ---- internal ----
|
||||
description: str
|
||||
default_status: str
|
||||
# ------------------
|
||||
|
||||
COL_NAMES = [
|
||||
'Status',
|
||||
'Name',
|
||||
'Version',
|
||||
'Repository',
|
||||
'Download Size (MB)',
|
||||
'Build',
|
||||
'Install',
|
||||
#'License',
|
||||
#'URL',
|
||||
#'Summary'
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def build(status, entry):
|
||||
cli_format = '%Y-%m-%d %H:%M:%S'
|
||||
buildtime = datetime.strptime(entry['buildtime'], cli_format)
|
||||
if entry['installtime']:
|
||||
installtime = datetime.strptime(entry['installtime'], cli_format)
|
||||
else:
|
||||
installtime = ZERO_DATE
|
||||
return Template(
|
||||
status,
|
||||
entry['name'],
|
||||
'%s:%s-%s' % (entry['epoch'], entry['version'], entry['release']),
|
||||
entry['reponame'],
|
||||
int(entry['size']) // 1000000,
|
||||
buildtime,
|
||||
installtime,
|
||||
#entry['license'],
|
||||
#entry['url'],
|
||||
entry['description'],
|
||||
status
|
||||
)
|
||||
|
||||
class Action(typing.NamedTuple):
|
||||
op: str
|
||||
name: str
|
||||
evr: str
|
||||
|
||||
TYPES = [str, str, str]
|
||||
COL_NAMES = ['Operation', 'Name', 'Version']
|
||||
|
||||
class TemplateStatusDelegate(PyQt5.QtWidgets.QStyledItemDelegate):
|
||||
OPS = [
|
||||
['Installed', 'Reinstall', 'Remove'],
|
||||
['Extra', 'Remove'],
|
||||
['Upgradable', 'Upgrade', 'Remove'],
|
||||
['Downgradable', 'Downgrade', 'Remove'],
|
||||
['Available', 'Install']
|
||||
]
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
_ = option # unused
|
||||
editor = PyQt5.QtWidgets.QComboBox(parent)
|
||||
# Otherwise the internalPointer can be overwritten with a QComboBox
|
||||
index = index.model().index(index.row(), index.column())
|
||||
kind = index.internalPointer().default_status
|
||||
for op_list in TemplateStatusDelegate.OPS:
|
||||
if op_list[0] == kind:
|
||||
for op in op_list:
|
||||
editor.addItem(op)
|
||||
editor.currentIndexChanged.connect(self.currentIndexChanged)
|
||||
editor.showPopup()
|
||||
return editor
|
||||
return None
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
#pylint: disable=no-self-use
|
||||
cur = index.data()
|
||||
idx = editor.findText(cur)
|
||||
if idx >= 0:
|
||||
editor.setCurrentIndex(idx)
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
#pylint: disable=no-self-use
|
||||
model.setData(index, editor.currentText())
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
#pylint: disable=no-self-use
|
||||
_ = index # unused
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
@PyQt5.QtCore.pyqtSlot()
|
||||
def currentIndexChanged(self):
|
||||
self.commitData.emit(self.sender())
|
||||
|
||||
class TemplateModel(PyQt5.QtCore.QAbstractItemModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.children = []
|
||||
|
||||
def flags(self, index):
|
||||
if index.isValid() and index.column() == 0:
|
||||
return super().flags(index) | PyQt5.QtCore.Qt.ItemIsEditable
|
||||
return super().flags(index)
|
||||
|
||||
def sort(self, idx, order):
|
||||
rev = (order == PyQt5.QtCore.Qt.AscendingOrder)
|
||||
self.children.sort(key=lambda x: x[idx], reverse=rev)
|
||||
|
||||
self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
|
||||
|
||||
def index(self, row, column, parent=PyQt5.QtCore.QModelIndex()):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return PyQt5.QtCore.QModelIndex()
|
||||
|
||||
return self.createIndex(row, column, self.children[row])
|
||||
|
||||
def parent(self, child):
|
||||
#pylint: disable=no-self-use
|
||||
_ = child # unused
|
||||
return PyQt5.QtCore.QModelIndex()
|
||||
|
||||
def rowCount(self, parent=PyQt5.QtCore.QModelIndex()):
|
||||
#pylint: disable=no-self-use
|
||||
_ = parent # unused
|
||||
return len(self.children)
|
||||
|
||||
def columnCount(self, parent=PyQt5.QtCore.QModelIndex()):
|
||||
#pylint: disable=no-self-use
|
||||
_ = parent # unused
|
||||
return len(Template.COL_NAMES)
|
||||
|
||||
def hasChildren(self, index=PyQt5.QtCore.QModelIndex()):
|
||||
#pylint: disable=no-self-use
|
||||
return index == PyQt5.QtCore.QModelIndex()
|
||||
|
||||
def data(self, index, role=PyQt5.QtCore.Qt.DisplayRole):
|
||||
if index.isValid():
|
||||
data = self.children[index.row()][index.column()]
|
||||
if role == PyQt5.QtCore.Qt.DisplayRole:
|
||||
if data is ZERO_DATE:
|
||||
return ''
|
||||
if isinstance(data, datetime):
|
||||
return data.strftime('%d %b %Y')
|
||||
return data
|
||||
if role == PyQt5.QtCore.Qt.FontRole:
|
||||
font = PyQt5.QtGui.QFont()
|
||||
tpl = self.children[index.row()]
|
||||
font.setBold(tpl.status != tpl.default_status)
|
||||
return font
|
||||
if role == PyQt5.QtCore.Qt.TextAlignmentRole:
|
||||
if isinstance(data, int):
|
||||
return PyQt5.QtCore.Qt.AlignRight
|
||||
return PyQt5.QtCore.Qt.AlignLeft
|
||||
return None
|
||||
|
||||
def setData(self, index, value, role=PyQt5.QtCore.Qt.EditRole):
|
||||
if index.isValid() and role == PyQt5.QtCore.Qt.EditRole:
|
||||
old_list = list(self.children[index.row()])
|
||||
old_list[index.column()] = value
|
||||
new_tpl = Template(*old_list)
|
||||
self.children[index.row()] = new_tpl
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
return False
|
||||
|
||||
def headerData(self, section, orientation,
|
||||
role=PyQt5.QtCore.Qt.DisplayRole):
|
||||
#pylint: disable=no-self-use
|
||||
if section < len(Template.COL_NAMES) \
|
||||
and orientation == PyQt5.QtCore.Qt.Horizontal \
|
||||
and role == PyQt5.QtCore.Qt.DisplayRole:
|
||||
return Template.COL_NAMES[section]
|
||||
return None
|
||||
|
||||
def removeRows(self, row, count, parent=PyQt5.QtCore.QModelIndex()):
|
||||
_ = parent # unused
|
||||
self.beginRemoveRows(PyQt5.QtCore.QModelIndex(), row, row + count)
|
||||
del self.children[row:row+count]
|
||||
self.endRemoveRows()
|
||||
self.dataChanged.emit(*self.row_index(row, row + count))
|
||||
|
||||
def row_index(self, low, high):
|
||||
return self.createIndex(low, 0), \
|
||||
self.createIndex(high, self.columnCount())
|
||||
|
||||
def set_templates(self, templates):
|
||||
self.removeRows(0, self.rowCount())
|
||||
cnt = sum(len(g) for _, g in templates.items())
|
||||
self.beginInsertRows(PyQt5.QtCore.QModelIndex(), 0, cnt - 1)
|
||||
for status, grp in templates.items():
|
||||
for tpl in grp:
|
||||
self.children.append(Template.build(status, tpl))
|
||||
self.endInsertRows()
|
||||
self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
|
||||
|
||||
def get_actions(self):
|
||||
actions = []
|
||||
for tpl in self.children:
|
||||
if tpl.status != tpl.default_status:
|
||||
actions.append(Action(tpl.status, tpl.name, tpl.evr))
|
||||
return actions
|
||||
|
||||
async def refresh(self, refresh=True):
|
||||
cmd = BASE_CMD[:]
|
||||
if refresh:
|
||||
# Force refresh if triggered by button press
|
||||
cmd.append('--refresh')
|
||||
cmd.extend(['info', '--machine-readable-json', '--installed',
|
||||
'--available', '--upgrades', '--extras'])
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE)
|
||||
output, stderr = await proc.communicate()
|
||||
output = output.decode('ASCII')
|
||||
if proc.returncode != 0:
|
||||
stderr = stderr.decode('ASCII')
|
||||
return False, stderr
|
||||
# Default type is dict as we're going to replace the lists with
|
||||
# dicts shortly after
|
||||
tpls = collections.defaultdict(dict, json.loads(output))
|
||||
# Remove duplicates
|
||||
# Should this be done in qvm-template?
|
||||
# TODO: Merge templates with same name?
|
||||
# If so, we may need to have a separate UI to force versions.
|
||||
local_names = set(x['name'] for x in tpls['installed'])
|
||||
# Convert to dict for easier subtraction
|
||||
for key in tpls:
|
||||
tpls[key] = {
|
||||
(x['name'], x['epoch'], x['version'], x['release']): x
|
||||
for x in tpls[key]}
|
||||
tpls['installed'] = {
|
||||
k: v for k, v in tpls['installed'].items()
|
||||
if k not in tpls['extra'] and k not in tpls['upgradable']}
|
||||
tpls['available'] = {
|
||||
k: v for k, v in tpls['available'].items()
|
||||
if k not in tpls['installed']
|
||||
and k not in tpls['upgradable']}
|
||||
# If the package name is installed but the specific version is
|
||||
# neither installed or an upgrade, then it must be a downgrade
|
||||
tpls['downgradable'] = {
|
||||
k: v for k, v in tpls['available'].items()
|
||||
if k[0] in local_names}
|
||||
tpls['available'] = {
|
||||
k: v for k, v in tpls['available'].items()
|
||||
if k not in tpls['downgradable']}
|
||||
# Convert back to list
|
||||
tpls = {k.title(): list(v.values()) for k, v in tpls.items()}
|
||||
self.set_templates(tpls)
|
||||
return True, None
|
||||
|
||||
class TemplateInstallConfirmDialog(
|
||||
ui_templateinstallconfirmdlg.Ui_TemplateInstallConfirmDlg,
|
||||
PyQt5.QtWidgets.QDialog):
|
||||
def __init__(self, actions):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
model = PyQt5.QtGui.QStandardItemModel()
|
||||
model.setHorizontalHeaderLabels(Action.COL_NAMES)
|
||||
self.treeView.setModel(model)
|
||||
|
||||
for act in actions:
|
||||
model.appendRow([PyQt5.QtGui.QStandardItem(x) for x in act])
|
||||
|
||||
class TemplateInstallProgressDialog(
|
||||
ui_templateinstallprogressdlg.Ui_TemplateInstallProgressDlg,
|
||||
PyQt5.QtWidgets.QDialog):
|
||||
def __init__(self, actions):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self.actions = actions
|
||||
self.buttonBox.hide()
|
||||
|
||||
@staticmethod
|
||||
def _process_cr(text):
|
||||
"""Reduce lines replaced using CR character (\r)"""
|
||||
while '\r' in text:
|
||||
prefix, suffix = text.rsplit('\r', 1)
|
||||
if '\n' in prefix:
|
||||
prefix = prefix.rsplit('\n', 1)[0]
|
||||
prefix += '\n'
|
||||
else:
|
||||
prefix = ''
|
||||
text = prefix + suffix
|
||||
return text
|
||||
|
||||
def install(self):
|
||||
async def coro():
|
||||
self.actions.sort()
|
||||
for oper, grp in itertools.groupby(self.actions, lambda x: x[0]):
|
||||
oper = oper.lower()
|
||||
# No need to specify versions for local operations
|
||||
if oper in ('remove', 'purge'):
|
||||
specs = [x.name for x in grp]
|
||||
else:
|
||||
specs = [x.name + '-' + x.evr for x in grp]
|
||||
# FIXME: (C)Python versions before 3.9 fully-buffers stderr in
|
||||
# this context, cf. https://bugs.python.org/issue13601
|
||||
# Forcing it to be unbuffered for the time being so that
|
||||
# the messages can be displayed in time.
|
||||
envs = os.environ.copy()
|
||||
envs['PYTHONUNBUFFERED'] = '1'
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*(BASE_CMD + [oper, '--'] + specs),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
env=envs)
|
||||
#pylint: disable=cell-var-from-loop
|
||||
status_text = ''
|
||||
while True:
|
||||
line = await proc.stdout.read(100)
|
||||
if line == b'':
|
||||
break
|
||||
line = line.decode('UTF-8')
|
||||
status_text = self._process_cr(status_text + line)
|
||||
self.textEdit.setPlainText(status_text)
|
||||
if await proc.wait() != 0:
|
||||
self.buttonBox.show()
|
||||
self.progressBar.setMaximum(100)
|
||||
self.progressBar.setValue(0)
|
||||
return False
|
||||
self.progressBar.setMaximum(100)
|
||||
self.progressBar.setValue(100)
|
||||
self.buttonBox.show()
|
||||
return True
|
||||
asyncio.create_task(coro())
|
||||
|
||||
class QvmTemplateWindow(
|
||||
ui_qvmtemplate.Ui_QubesTemplateManager,
|
||||
PyQt5.QtWidgets.QMainWindow):
|
||||
def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
|
||||
_ = parent # unused
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self.listing.header().setSectionResizeMode(
|
||||
PyQt5.QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
self.qubes_app = qubes_app
|
||||
self.qt_app = qt_app
|
||||
self.dispatcher = dispatcher
|
||||
|
||||
self.listing_model = TemplateModel()
|
||||
self.listing_delegate = TemplateStatusDelegate(self.listing)
|
||||
|
||||
self.listing.setModel(self.listing_model)
|
||||
self.listing.setItemDelegateForColumn(0, self.listing_delegate)
|
||||
|
||||
self.refresh(False)
|
||||
self.listing.setItemDelegateForColumn(0, self.listing_delegate)
|
||||
self.listing.selectionModel() \
|
||||
.selectionChanged.connect(self.update_info)
|
||||
|
||||
self.actionRefresh.triggered.connect(lambda: self.refresh(True))
|
||||
self.actionInstall.triggered.connect(self.do_install)
|
||||
|
||||
def update_info(self, selected):
|
||||
_ = selected # unused
|
||||
indices = [
|
||||
x
|
||||
for x in self.listing.selectionModel().selectedIndexes()
|
||||
if x.column() == 0]
|
||||
if len(indices) == 0:
|
||||
return
|
||||
self.infobox.clear()
|
||||
cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
|
||||
bold_fmt = PyQt5.QtGui.QTextCharFormat()
|
||||
bold_fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
|
||||
norm_fmt = PyQt5.QtGui.QTextCharFormat()
|
||||
if len(indices) > 1:
|
||||
cursor.insertText('Selected templates:\n', bold_fmt)
|
||||
for idx in indices:
|
||||
tpl = self.listing_model.children[idx.row()]
|
||||
cursor.insertText(tpl.name + '-' + tpl.evr + '\n', norm_fmt)
|
||||
else:
|
||||
idx = indices[0]
|
||||
tpl = self.listing_model.children[idx.row()]
|
||||
cursor.insertText('Name: ', bold_fmt)
|
||||
cursor.insertText(tpl.name + '\n', norm_fmt)
|
||||
cursor.insertText('Description:\n', bold_fmt)
|
||||
cursor.insertText(tpl.description + '\n', norm_fmt)
|
||||
|
||||
def refresh(self, refresh=True):
|
||||
self.progressBar.show()
|
||||
async def coro():
|
||||
ok, stderr = await self.listing_model.refresh(refresh)
|
||||
self.infobox.clear()
|
||||
if not ok:
|
||||
cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
|
||||
fmt = PyQt5.QtGui.QTextCharFormat()
|
||||
fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
|
||||
cursor.insertText('Failed to fetch template list:\n', fmt)
|
||||
fmt.setFontWeight(PyQt5.QtGui.QFont.Normal)
|
||||
cursor.insertText(stderr, fmt)
|
||||
self.progressBar.hide()
|
||||
asyncio.create_task(coro())
|
||||
|
||||
def do_install(self):
|
||||
actions = self.listing_model.get_actions()
|
||||
confirm = TemplateInstallConfirmDialog(actions)
|
||||
if confirm.exec_():
|
||||
progress = TemplateInstallProgressDialog(actions)
|
||||
progress.install()
|
||||
progress.exec_()
|
||||
self.refresh()
|
||||
|
||||
def main():
|
||||
utils.run_asynchronous(QvmTemplateWindow)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -64,6 +64,7 @@ rm -rf $RPM_BUILD_ROOT
|
||||
/usr/bin/qubes-qube-manager
|
||||
/usr/bin/qubes-log-viewer
|
||||
/usr/bin/qubes-template-manager
|
||||
/usr/bin/qvm-template-gui
|
||||
/usr/libexec/qubes-manager/mount_for_backup.sh
|
||||
/usr/libexec/qubes-manager/qvm_about.sh
|
||||
/usr/libexec/qubes-manager/dsa-4371-update
|
||||
@ -92,6 +93,7 @@ rm -rf $RPM_BUILD_ROOT
|
||||
%{python3_sitelib}/qubesmanager/bootfromdevice.py
|
||||
%{python3_sitelib}/qubesmanager/device_list.py
|
||||
%{python3_sitelib}/qubesmanager/template_manager.py
|
||||
%{python3_sitelib}/qubesmanager/qvm_template_gui.py
|
||||
|
||||
%{python3_sitelib}/qubesmanager/resources_rc.py
|
||||
|
||||
@ -111,6 +113,9 @@ rm -rf $RPM_BUILD_ROOT
|
||||
%{python3_sitelib}/qubesmanager/ui_devicelist.py
|
||||
%{python3_sitelib}/qubesmanager/ui_templatemanager.py
|
||||
%{python3_sitelib}/qubesmanager/ui_clonevmdlg.py
|
||||
%{python3_sitelib}/qubesmanager/ui_qvmtemplate.py
|
||||
%{python3_sitelib}/qubesmanager/ui_templateinstallconfirmdlg.py
|
||||
%{python3_sitelib}/qubesmanager/ui_templateinstallprogressdlg.py
|
||||
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm
|
||||
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts
|
||||
|
||||
|
3
setup.py
3
setup.py
@ -27,6 +27,7 @@ if __name__ == '__main__':
|
||||
'qubes-backup-restore = qubesmanager.restore:main',
|
||||
'qubes-qube-manager = qubesmanager.qube_manager:main',
|
||||
'qubes-log-viewer = qubesmanager.log_dialog:main',
|
||||
'qubes-template-manager = qubesmanager.template_manager:main'
|
||||
'qubes-template-manager = qubesmanager.template_manager:main',
|
||||
'qvm-template-gui = qubesmanager.qvm_template_gui:main'
|
||||
],
|
||||
})
|
||||
|
114
ui/qvmtemplate.ui
Normal file
114
ui/qvmtemplate.ui
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>QubesTemplateManager</class>
|
||||
<widget class="QMainWindow" name="QubesTemplateManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>930</width>
|
||||
<height>478</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Qubes Template Manager</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="maximum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Select actions to perform in "Status" column:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="listing">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::AllEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="infobox">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Use "Status" column to select actions to execute on a given template; when desired actions are selected, use "Apply" to perform them.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>930</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
<property name="windowTitle">
|
||||
<string>toolBar</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionRefresh"/>
|
||||
<addaction name="actionInstall"/>
|
||||
</widget>
|
||||
<action name="actionRefresh">
|
||||
<property name="icon">
|
||||
<iconset theme="view-refresh">
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Refresh</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionInstall">
|
||||
<property name="icon">
|
||||
<iconset theme="go-down">
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
84
ui/templateinstallconfirmdlg.ui
Normal file
84
ui/templateinstallconfirmdlg.ui
Normal file
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TemplateInstallConfirmDlg</class>
|
||||
<widget class="QDialog" name="TemplateInstallConfirmDlg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>290</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Template Install Confirmation</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">WARNING: Local changes made to the following templates will be overwritten! Continue?</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="treeView">
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>TemplateInstallConfirmDlg</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>TemplateInstallConfirmDlg</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
81
ui/templateinstallprogressdlg.ui
Normal file
81
ui/templateinstallprogressdlg.ui
Normal file
@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TemplateInstallProgressDlg</class>
|
||||
<widget class="QDialog" name="TemplateInstallProgressDlg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>840</width>
|
||||
<height>260</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Installing Templates...</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="maximum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>TemplateInstallProgressDlg</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>TemplateInstallProgressDlg</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
Loading…
Reference in New Issue
Block a user