"""The main RKView class and associated widgets.
"""
import os
from pathlib import Path
from rkviewer.plugin.classes import CATEGORY_NAMES, PluginCategory
from typing import Any, Callable, Dict, List, Optional, Tuple
import json
# pylint: disable=maybe-no-member
# pylint: disable=no-name-in-module
import wx
from wx.lib.buttons import GenBitmapButton, GenBitmapTextButton
import wx.lib.agw.flatnotebook as fnb
from commentjson.commentjson import JSONLibraryException
from rkviewer.plugin.api import init_api
import wx.adv
import rkviewer
from rkviewer.canvas.geometry import get_bounding_rect
from rkviewer.plugin_manage import PluginManager
from .canvas.canvas import Alignment, Canvas
from .canvas.data import Compartment, Node, Reaction
from .canvas.state import InputMode, cstate
from .config import (DEFAULT_SETTING_FMT, INIT_SETTING_TEXT, get_default_raw_settings, get_setting, get_theme,
GetConfigDir, GetThemeSettingsPath, load_theme_settings, pop_settings_err, runtime_vars)
from .events import (CanvasDidUpdateEvent, DidMoveCompartmentsEvent,
DidMoveNodesEvent, DidResizeCompartmentsEvent,
DidResizeNodesEvent, SelectionDidUpdateEvent,
bind_handler)
from .forms import CompartmentForm, NodeForm, ReactionForm
from .mvc import IController, IView
from .utils import ButtonGroup, on_msw, resource_path, start_file
from rkviewer.config import AppSettings
[docs]class EditPanel(fnb.FlatNotebook):
"""Panel that displays and allows editing of the details of a node.
Attributes
node_form: The actual form widget. This is at the same level as null_message. TODO
null_message: The widget displayed in place of the form, when nothing is selected.
"""
node_form: NodeForm
reaction_form: ReactionForm
comp_form: CompartmentForm
null_message: wx.Panel
def __init__(self, parent, canvas: Canvas, controller: IController, **kw):
FNB_STYLE = fnb.FNB_NO_X_BUTTON | fnb.FNB_NO_NAV_BUTTONS | fnb.FNB_NODRAG | fnb.FNB_DROPDOWN_TABS_LIST | fnb.FNB_RIBBON_TABS
super().__init__(parent, agwStyle=FNB_STYLE, **kw)
self.SetTabAreaColour(get_theme('toolbar_bg'))
self.SetNonActiveTabTextColour(get_theme('toolbar_fg'))
self.SetActiveTabTextColour(get_theme('active_tab_fg'))
# self.SetActiveTabColour(get_theme('active_tab_bg'))
self.canvas = canvas
self.node_form = NodeForm(self, canvas, controller)
self.reaction_form = ReactionForm(self, canvas, controller)
self.comp_form = CompartmentForm(self, canvas, controller)
self.null_message = wx.Panel(self)
self.null_message.SetForegroundColour(get_theme('toolbar_fg'))
self.SetBackgroundColour(get_theme('toolbar_bg'))
self.null_message.SetBackgroundColour(get_theme('toolbar_bg'))
text = wx.StaticText(
self.null_message, label="Nothing is selected.", style=wx.ALIGN_CENTER)
null_sizer = wx.BoxSizer(wx.HORIZONTAL)
null_sizer.Add(text, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
self.null_message.SetSizer(null_sizer)
self.SetCustomPage(self.null_message)
self.node_form.Hide()
self.reaction_form.Hide()
self.comp_form.Hide()
# overall sizer for alternating form and "nothing selected" displays
#sizer = wx.BoxSizer(wx.HORIZONTAL)
#sizer.Add(null_message, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
# self.SetSizer(sizer)
bind_handler(CanvasDidUpdateEvent, self.OnCanvasDidUpdate)
bind_handler(SelectionDidUpdateEvent, self.OnSelectionDidUpdate)
bind_handler(DidMoveNodesEvent, self.OnNodesDidMove)
bind_handler(DidResizeNodesEvent, self.OnDidResizeNodes)
bind_handler(DidMoveCompartmentsEvent, self.OnCompartmentsDidMove)
bind_handler(DidResizeCompartmentsEvent, self.OnDidResizeCompartments)
[docs] def OnCanvasDidUpdate(self, evt):
self.node_form.UpdateNodes(self.canvas.nodes)
self.reaction_form.CanvasUpdated(self.canvas.reactions, self.canvas.nodes)
self.comp_form.UpdateCompartments(self.canvas.compartments)
[docs] def OnSelectionDidUpdate(self, evt):
focused = self.GetTopLevelParent().FindFocus()
should_show_nodes = len(evt.node_indices) != 0
should_show_reactions = len(evt.reaction_indices) != 0
should_show_comps = len(evt.compartment_indices) != 0
need_update_nodes = self.node_form.selected_idx != evt.node_indices
need_update_comps = self.comp_form.selected_idx != evt.compartment_indices
cur_page = self.GetCurrentPage()
node_index = -1
for i in range(self.GetPageCount()):
if self.GetPage(i) == self.node_form:
node_index = i
break
if need_update_nodes or need_update_comps:
self.node_form.UpdateSelection(evt.node_indices, comps_selected=should_show_comps)
if should_show_nodes:
if node_index == -1:
self.InsertPage(0, self.node_form, 'Nodes')
elif node_index != -1:
# find and remove existing page
self.RemovePage(node_index)
self.node_form.Hide()
reaction_index = -1
for i in range(self.GetPageCount()):
if self.GetPage(i) == self.reaction_form:
reaction_index = i
break
if self.reaction_form.selected_idx != evt.reaction_indices:
self.reaction_form.UpdateSelection(evt.reaction_indices)
if should_show_reactions:
if reaction_index == -1:
self.AddPage(self.reaction_form, 'Reactions')
elif reaction_index != -1:
self.RemovePage(reaction_index)
self.reaction_form.Hide()
comp_index = -1
for i in range(self.GetPageCount()):
if self.GetPage(i) == self.comp_form:
comp_index = i
break
if need_update_comps or need_update_nodes:
self.comp_form.UpdateSelection(evt.compartment_indices,
nodes_selected=should_show_nodes)
if should_show_comps:
if comp_index == -1:
self.AddPage(self.comp_form, 'Compartments')
elif comp_index != -1:
self.RemovePage(comp_index)
self.comp_form.Hide()
# set the active tab to the same as before
if cur_page is not None and self.GetCurrentPage() is not None:
if cur_page is self.GetCurrentPage():
self.AdvanceSelection()
# need to reset focus to canvas, since for some reason FlatNotebook sets focus to the first
# field in a notebook page after it is added.
self.GetSizer().Layout()
if focused:
# restore focus, since otherwise for some reason the newly added page gets the focus
focused.SetFocus()
# need to manually show this for some reason
if not should_show_nodes and not should_show_reactions and not should_show_comps:
self.null_message.Show()
[docs] def OnNodesDidMove(self, evt):
self.node_form.NodesMovedOrResized(evt)
[docs] def OnDidResizeNodes(self, evt):
self.node_form.NodesMovedOrResized(evt)
[docs] def OnCompartmentsDidMove(self, evt):
self.comp_form.CompsMovedOrResized(evt)
[docs] def OnDidResizeCompartments(self, evt):
self.comp_form.CompsMovedOrResized(evt)
[docs]class ModePanel(wx.Panel):
"""ModePanel at the left of the app."""
def __init__(self, *args, toggle_callback, canvas: Canvas, **kw):
super().__init__(*args, **kw)
self.btn_group = ButtonGroup(toggle_callback)
sizer = wx.BoxSizer(wx.VERTICAL)
self.AppendModeButton('Select', InputMode.SELECT, sizer)
self.AppendModeButton('+Nodes', InputMode.ADD_NODES, sizer)
self.AppendModeButton('+Compts', InputMode.ADD_COMPARTMENTS, sizer)
self.AppendModeButton('Zoom', InputMode.ZOOM, sizer)
self.AppendSeparator(sizer)
self.AppendNormalButton('Reactants', canvas.MarkSelectedAsReactants,
sizer, tooltip='Mark selected nodes as reactants')
self.AppendNormalButton('Products', canvas.MarkSelectedAsProducts,
sizer, tooltip='Mark selected nodes as products')
self.AppendNormalButton('Create Rxn', canvas.CreateReactionFromMarked,
sizer, tooltip='Create reaction from marked reactants and products')
self.SetSizer(sizer)
[docs] def AppendSeparator(self, sizer: wx.Sizer):
sizer.Add((0, 10))
[docs]class BottomBar(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
self.SetForegroundColour(get_theme('toolbar_fg'))
self.SetBackgroundColour(get_theme('toolbar_bg'))
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.SetSizer(self.sizer)
[docs] def CreateSlider(self):
self.sizer.Add((0, 0), proportion=1, flag=wx.EXPAND)
zoom_slider = wx.Slider(self, style=wx.SL_BOTTOM | wx.SL_AUTOTICKS, size=(225, 25))
self.sizer.Add(zoom_slider, wx.SizerFlags().Align(wx.ALIGN_CENTER_VERTICAL))
self.sizer.Layout()
return zoom_slider
[docs]class MainPanel(wx.Panel):
"""The main panel, which is the only chlid of the root Frame."""
# controller: IController
# canvas: Canvas
# mode_panel: ModePanel
# toolbar: TabbedToolbar
# edit_panel: EditPanel
# last_save_path: Optional[str]
def __init__(self, parent, controller: IController, manager: PluginManager):
# ensure the parent's __init__ is called
super().__init__(parent, style=wx.CLIP_CHILDREN)
self.SetBackgroundColour(get_theme('overall_bg'))
self.controller = controller
self.bottom_bar = BottomBar(self)
zoom_slider = self.bottom_bar.CreateSlider()
self.canvas = Canvas(self.controller, zoom_slider, self,
size=(get_theme('canvas_width'),
get_theme('canvas_height')),
realsize=(4 * get_theme('canvas_width'),
4 * get_theme('canvas_height')),)
self.canvas.SetScrollRate(10, 10)
# The bg of the available canvas will be drawn by canvas in OnPaint()
self.canvas.SetBackgroundColour(get_theme('canvas_outside_bg'))
def set_input_mode(ident): cstate.input_mode = ident
# create a panel in the frame
self.mode_panel = ModePanel(self,
size=(get_theme('mode_panel_width'),
get_theme('canvas_height')),
toggle_callback=set_input_mode,
canvas=self.canvas,
)
self.mode_panel.SetForegroundColour(get_theme('toolbar_fg'))
self.mode_panel.SetBackgroundColour(get_theme('toolbar_bg'))
# Note: setting the width to 0 doesn't matter since GridBagSizer is in control of the
# width.
self.toolbar = TabbedToolbar(self, controller, self.canvas,
self.ToggleEditPanel, manager,
size=(0, get_theme('toolbar_height')))
# listview = self.toolbar.GetListView()
# listview.SetFont(wx.Font(wx.FontInfo(10.5)))
# listview.SetForegroundColour(get_theme('toolbar_fg'))
# listview.SetBackgroundColour(get_theme('toolbar_bg'))
# listview.SetSize(100, 200)
self.toolbar.SetForegroundColour(get_theme('toolbar_fg'))
self.toolbar.SetBackgroundColour(get_theme('toolbar_bg'))
self.toolbar.SetTabAreaColour(get_theme('toolbar_bg'))
self.toolbar.SetNonActiveTabTextColour(get_theme('toolbar_fg'))
self.toolbar.SetActiveTabTextColour(get_theme('active_tab_fg'))
# self.toolbar.SetActiveTabColour(get_theme('active_tab_bg'))
self.edit_panel = EditPanel(self, self.canvas, self.controller,
size=(get_theme('edit_panel_width'),
get_theme('canvas_height')))
# and create a sizer to manage the layout of child widgets
sizer = wx.GridBagSizer(vgap=get_theme('vgap'), hgap=get_theme('hgap'))
sizer.Add(self.toolbar, wx.GBPosition(0, 0), wx.GBSpan(1, 3), flag=wx.EXPAND)
sizer.Add(self.mode_panel, wx.GBPosition(1, 0), flag=wx.EXPAND)
sizer.Add(self.canvas, wx.GBPosition(1, 1), flag=wx.EXPAND)
sizer.Add(self.edit_panel, wx.GBPosition(1, 2), wx.GBSpan(2, 1), flag=wx.EXPAND)
sizer.Add(self.bottom_bar, wx.GBPosition(2, 0), wx.GBSpan(1, 2), flag=wx.EXPAND)
# allow the canvas to grow
sizer.AddGrowableCol(1, 1)
sizer.AddGrowableRow(1, 1)
# Set the sizer and *prevent the user from resizing it to a smaller size
self.SetSizerAndFit(sizer)
self.last_save_path = None
[docs] def ToggleEditPanel(self):
sizer = self.GetSizer()
if self.edit_panel.IsShown():
sizer.Detach(self.edit_panel)
sizer.SetItemSpan(self.canvas, wx.GBSpan(1, 2))
self.edit_panel.Hide()
else:
sizer.SetItemSpan(self.canvas, wx.GBSpan(1, 1))
sizer.Add(self.edit_panel, wx.GBPosition(1, 2), flag=wx.EXPAND)
self.edit_panel.Show()
self.Layout()
[docs]class NetworkPrintout(wx.Printout):
def __init__(self, img: wx.Image):
super().__init__()
self.image = img
[docs] def OnPrintPage(self, pageNum: int):
if pageNum > 1:
return False
self.FitThisSizeToPage(self.image.GetSize())
dc = self.GetDC()
assert dc.CanDrawBitmap()
dc.DrawBitmap(wx.Bitmap(self.image), wx.Point(0, 0))
return True
[docs]class MainFrame(wx.Frame):
"""The main frame."""
# save_item: wx.MenuItem
def __init__(self, controller: IController, **kw):
super().__init__(None, style=wx.DEFAULT_FRAME_STYLE |
wx.WS_EX_PROCESS_UI_UPDATES, **kw)
self.last_save_path = None
manager = PluginManager(self, controller)
load_theme_settings()
self.appSettings = AppSettings()
self.appSettings.load_appSettings()
self.manager = manager
status_fields = get_setting('status_fields')
assert status_fields is not None
self.CreateStatusBar(len(get_setting('status_fields')))
self.SetStatusWidths([width for _, width in status_fields])
self.main_panel = MainPanel(self, controller, manager)
self.manager.bind_error_callback(lambda msg: self.main_panel.canvas.ShowWarningDialog(msg, caption='Plugin Error'))
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.main_panel, 1, wx.EXPAND)
self.Bind(wx.EVT_SHOW, self.OnShow)
canvas = self.main_panel.canvas
self.controller = controller
self.canvas = canvas
def add_item(menu: wx.Menu, menu_name, callback):
id_ = menu.Append(-1, menu_name).Id
menu.Bind(wx.EVT_MENU, lambda _: callback(), id=id_)
entries = list()
menu_bar = wx.MenuBar()
self.menu_events = list()
file_menu = wx.Menu()
self.AddMenuItem(file_menu, '&New', 'Start a new network',
lambda _: self.NewNetwork(), entries, key=(wx.ACCEL_CTRL, ord('N')))
file_menu.AppendSeparator()
self.AddMenuItem(file_menu, '&Load...', 'Load network from JSON file',
lambda _: self.LoadFromJson(), entries, key=(wx.ACCEL_CTRL, ord('O')))
# TODO Load Recent...
file_menu.AppendSeparator()
self.save_item = self.AddMenuItem(file_menu, '&Save', 'Save current network as a JSON file',
lambda _: self.SaveJson(), entries, key=(wx.ACCEL_CTRL, ord('S')))
self.save_item.Enable(False)
self.AddMenuItem(file_menu, '&Save As...', 'Save current network as a JSON file',
lambda _: self.SaveAsJson(), entries, key=(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('N')))
file_menu.AppendSeparator()
self.AddMenuItem(file_menu, '&Edit Settings', 'Edit settings',
lambda _: self.EditSettings(), entries)
self.AddMenuItem(file_menu, '&Reload Settings', 'Reload settings',
lambda _: self.ReloadSettings(), entries)
file_menu.AppendSeparator()
align_menu = wx.Menu()
add_item(align_menu, 'Export .png...',
lambda: self.ExportAs(wx.BITMAP_TYPE_PNG, 'PNG', 'PNG files (.png)|*.png'))
add_item(align_menu, 'Export .jpg...',
lambda: self.ExportAs(wx.BITMAP_TYPE_JPEG, 'JPEG', 'JPEG files (.jpg)|*.jpg'))
add_item(align_menu, 'Export .bmp...',
lambda: self.ExportAs(wx.BITMAP_TYPE_BMP, 'BMP', 'BMP files (.bmp)|*.bmp'))
file_menu.AppendSubMenu(align_menu, '&Export As...')
file_menu.AppendSeparator()
self.AddMenuItem(file_menu, '&Print...', 'Print Network',
lambda _: self.PrintNetwork(), entries, key=(wx.ACCEL_CTRL, ord('P')))
file_menu.AppendSeparator()
self.AddMenuItem(file_menu, 'E&xit', 'Exit application',
lambda _: self.Close(), entries, id_=wx.ID_EXIT)
edit_menu = wx.Menu()
self.AddMenuItem(edit_menu, '&Undo', 'Undo action', lambda _: controller.undo(),
entries, key=(wx.ACCEL_CTRL, ord('Z')))
self.AddMenuItem(edit_menu, '&Redo', 'Redo action', lambda _: controller.redo(),
entries, key=(wx.ACCEL_CTRL, ord('Y')))
edit_menu.AppendSeparator()
self.AddMenuItem(edit_menu, '&Copy', 'Copy selected nodes', lambda _: canvas.CopySelected(),
entries, key=(wx.ACCEL_CTRL, ord('C')))
self.AddMenuItem(edit_menu, '&Paste', 'Paste selected nodes',
lambda _: canvas.Paste(), entries, key=(wx.ACCEL_CTRL, ord('V')))
self.AddMenuItem(edit_menu, '&Cut', 'Cut selected nodes',
lambda _: canvas.CutSelected(), entries, key=(wx.ACCEL_CTRL, ord('X')))
edit_menu.AppendSeparator()
self.AddMenuItem(edit_menu, '&Delete selected', 'Deleted selected',
lambda _: canvas.DeleteSelectedItems(), entries,
key=(wx.ACCEL_NORMAL, wx.WXK_DELETE))
select_menu = wx.Menu()
self.AddMenuItem(select_menu, 'Select &All', 'Select all',
lambda _: canvas.SelectAll(), entries, key=(wx.ACCEL_CTRL, ord('A')))
self.AddMenuItem(select_menu, 'Select All &Nodes', 'Select all nodes',
lambda _: canvas.SelectAllNodes(), entries, key=(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('N')))
self.AddMenuItem(select_menu, 'Select All &Reactions', 'Select all reactions',
lambda _: canvas.SelectAllReactions(), entries, key=(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('R')))
self.AddMenuItem(select_menu, 'Clear Selection', 'Clear the current selection',
lambda _: canvas.ClearCurrentSelection(), entries,
key=(wx.ACCEL_NORMAL, wx.WXK_ESCAPE))
view_menu = wx.Menu()
self.AddMenuItem(view_menu, 'Zoom &In', 'Zoom in canvas', lambda _: canvas.ZoomCenter(True),
entries, key=(wx.ACCEL_CTRL, ord('+')))
self.AddMenuItem(view_menu, 'Zoom &Out', 'Zoom out canvas',
lambda _: canvas.ZoomCenter(False), entries, key=(wx.ACCEL_CTRL, ord('-')))
self.AddMenuItem(view_menu, '&Reset Zoom', 'Reset canva zoom',
lambda _: canvas.ResetZoom(), entries, key=(wx.ACCEL_CTRL, ord(' ')))
canvas_menu = wx.Menu()
self.AddMenuItem(canvas_menu, '&Fit all node size to text',
'Fit the size of every node to its containing text',
lambda _: canvas.FitNodeSizeToText(), entries,
key=(wx.ACCEL_ALT | wx.ACCEL_SHIFT, ord('F')))
reaction_menu = wx.Menu()
self.AddMenuItem(reaction_menu, 'Mark Selected as &Reactants',
'Mark selected nodes as reactants',
lambda _: canvas.MarkSelectedAsReactants(), entries,
key=(wx.ACCEL_NORMAL, ord('S')))
self.AddMenuItem(reaction_menu, 'Mark Selected as &Products',
'Mark selected nodes as products',
lambda _: canvas.MarkSelectedAsProducts(), entries,
key=(wx.ACCEL_NORMAL, ord('F')))
self.AddMenuItem(reaction_menu, '&Create Reaction From Selected',
'Create reaction from selected sources and targets',
lambda _: canvas.CreateReactionFromMarked(), entries,
key=(wx.ACCEL_CTRL, ord('R')))
self.plugins_menu = wx.Menu()
self.AddMenuItem(self.plugins_menu, '&Plugins...', 'Manage plugins', self.ManagePlugins, entries,
key=(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('P')))
# load the plugin items in OnShow
self.plugins_menu.AppendSeparator()
help_menu = wx.Menu()
self.AddMenuItem(help_menu, '&About...',
'Show about dialog', self.onAboutDlg, entries) # self.ShowAbout, entries)
self.AddMenuItem(help_menu, '&Default settings...', 'Viewer default settings',
lambda _: self.ShowDefaultSettings(), entries)
menu_bar.Append(file_menu, '&File')
menu_bar.Append(edit_menu, '&Edit')
menu_bar.Append(select_menu, '&Select')
menu_bar.Append(view_menu, '&View')
menu_bar.Append(canvas_menu, '&Canvas')
menu_bar.Append(reaction_menu, '&Reaction')
menu_bar.Append(self.plugins_menu, '&Plugins')
menu_bar.Append(help_menu, '&Help')
atable = wx.AcceleratorTable(entries)
self.SetMenuBar(menu_bar)
self.atable = atable
canvas.SetAcceleratorTable(atable)
self.OverrideAccelTable(self)
self.Bind(wx.EVT_CLOSE, self.OnCloseExit)
# set sizer at the end, after adding the menus.
self.SetSizerAndFit(sizer)
self.SetSize(self.appSettings.size)
self.SetPosition(self.appSettings.position)
self.Layout()
# Record the initial position of the window
self.controller.set_application_position(self.GetPosition())
[docs] def OnShow(self, evt):
if runtime_vars().enable_plugins:
self.manager.load_from('plugins')
self.manager.register_menu(self.plugins_menu)
self.main_panel.toolbar.AddPluginPages()
evt.Skip()
# Anything we need to do when the app closes can be included here
[docs] def OnCloseExit(self, evt):
self.appSettings.size = self.GetSize()
self.appSettings.position = self.Position
self.appSettings.save_appSettings()
self.Destroy()
[docs] def AddMenuItem(self, menu: wx.Menu, text: str, help_text: str, callback: Callable,
entries: List, key: Tuple[Any, int] = None, id_: int = None) -> wx.MenuItem:
if id_ is None:
id_ = wx.NewIdRef(count=1)
shortcut = ''
if key is not None:
entry = wx.AcceleratorEntry(key[0], key[1], id_)
entries.append(entry)
shortcut = entry.ToString()
item = menu.Append(id_, '{}\t{}'.format(text, shortcut), help_text)
self.Bind(wx.EVT_MENU, callback, item)
self.menu_events.append((callback, item))
return item
[docs] def onAboutDlg(self, event):
info = wx.adv.AboutDialogInfo()
info.Name = "An Extensible Reaction Network Editor"
info.Version = "0.0.1 Beta"
info.Copyright = "(C) 2020"
info.Description = "Create reaction networks"
info.SetWebSite("https://github.com/evilnose/PyRKViewer",
"Home Page") # TODO update home page?
info.Developers = ["Gary Geng, Jin Xu, Carmen Pereña Cortés, Herbert Sauro"] # TODO update authors
info.License = "MIT"
# Show the wx.AboutBox
wx.adv.AboutBox(info)
[docs] def ReloadSettings(self):
load_theme_settings()
err = pop_settings_err()
if err is None:
# msg = NotificationMessage('Settings reloaded', 'Some changes may not be applied until the application is restarted.')
# msg.Show()
pass
else:
if isinstance(err, JSONLibraryException):
message = 'Failed when parsing settings.json.\n\n'
message += err.message
else:
message = 'Invalid settings in settings.json.\n\n'
message += str(err)
message += str(err)
self.main_panel.canvas.ShowWarningDialog(message)
[docs] def EditSettings(self):
"""Open the preferences file for editing."""
if not self.CreateConfigDir(GetConfigDir()):
return
if not os.path.exists(GetThemeSettingsPath()):
with open(GetThemeSettingsPath(), 'w') as fp:
fp.write(INIT_SETTING_TEXT)
else:
if not os.path.isfile(GetThemeSettingsPath()):
self.main_panel.canvas.ShowWarningDialog('Could not open settings file since '
'a directory already exists at path '
'{}.'.format(GetThemeSettingsPath()))
return
# If we're running windows use notepad
if os.name == 'nt':
# Doing it this way allows python to regain control even though notepad hasn't been clsoed
import subprocess
_pid = subprocess.Popen(['notepad.exe', GetThemeSettingsPath()]).pid
else:
start_file(GetThemeSettingsPath())
[docs] def CreateConfigDir(self, config_dir: str):
"""Create the configuration directory if it does not already exist."""
try:
sp = wx.StandardPaths.Get()
config_dir = sp.GetUserConfigDir()
if not os.path.exists(os.path.join(config_dir, 'rkViewer')):
config_dir = os.path.join(config_dir, 'rkViewer')
Path(config_dir).mkdir(parents=True, exist_ok=True)
else:
config_dir = os.path.join(os.path.join(config_dir, 'rkViewer'))
return True
except FileExistsError:
# TODO fix
self.main_panel.canvas.ShowWarningDialog('Could not create RKViewer configuration '
'directory. A file already exists at path '
'{}.'.format(config_dir))
return False
[docs] def ShowDefaultSettings(self):
if not self.CreateConfigDir(GetConfigDir()):
return
if os.path.exists(os.path.join(GetConfigDir(), 'rkViewer', '.default-settings.json')) and \
not os.path.isfile(os.path.join(GetConfigDir(), 'rkViewer', '.default-settings.json')):
self.main_panel.canvas.ShowWarningDialog('Could not open default settings file '
'since a directory already exists at path '
'{}.'.format(os.path.join(GetConfigDir(),
'rkViewer', '.default-settings.json')))
return
# TODO prepopulate file with help text, i.e link to docs about schema
json_str = json.dumps(get_default_raw_settings(), indent=4, sort_keys=True)
with open(os.path.join(GetConfigDir(), 'rkViewer', '.default-settings.json'), 'w') as fp:
fp.write(DEFAULT_SETTING_FMT.format(json_str))
# If we're running windows use notepad
if os.name == 'nt':
# Doing it this way allows python to regain control even though notepad hasn't been clsoed
import subprocess
_pid = subprocess.Popen(['notepad.exe', os.path.join(
GetConfigDir(), 'rkViewer', '.default-settings.json')]).pid
else:
start_file(os.path.join(GetConfigDir(), '.default-settings.json'))
[docs] def NewNetwork(self):
self.save_item.Enable()
self.controller.new_network()
[docs] def PrintNetwork(self):
img = self._GetExportImage()
if not img:
return
# Pass two printout objects: for preview, and possible printing.
printer = wx.Printer()
printout = NetworkPrintout(img)
printer.Print(self, printout, True)
[docs] def ExportNetwork(self):
self.main_panel.canvas.ShowWarningDialog("Export not yet implemented")
def _GetExportImage(self) -> Optional[wx.Image]:
img = self.main_panel.canvas.DrawActiveRectToImage()
if img is None:
self.canvas.ShowWarningDialog(
'There are no relevant elements (nodes/reactions/compartments) on the canvas! Print aborted.')
return None
return img
[docs] def ExportAs(self, btype, type_name: str, wildcard: str):
"""Export as the type given by btype (wx.BitmapType). btype is passed to SaveFile()
"""
img = self._GetExportImage()
if img is None:
return
with wx.FileDialog(self, "Save {} file".format(type_name), wildcard=wildcard,
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
pathname = fileDialog.GetPath()
try:
net_index = 0
net_json = self.controller.dump_network(net_index)
with open(pathname, 'w') as file:
json.dump(net_json, file, sort_keys=True, indent=4)
# Allow Save action, since we now know where to save to
self.last_save_path = pathname
self.save_item.Enable()
except IOError:
wx.LogError("Cannot save current data in file '{}'.".format(pathname))
img.SaveFile(pathname, type=btype)
[docs] def SaveJson(self):
if self.last_save_path is None:
return self.SaveAsJson()
try:
net_index = 0
net_json = self.controller.dump_network(net_index)
with open(self.last_save_path, 'w') as file:
json.dump(net_json, file, sort_keys=True, indent=4)
except IOError:
wx.LogError("Cannot save current data in file '{}'.".format(self.last_save_path))
[docs] def SaveAsJson(self):
with wx.FileDialog(self, "Save JSON file", wildcard="JSON files (*.json)|*.json",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# save the current contents in the file
pathname = fileDialog.GetPath()
try:
net_index = 0
net_json = self.controller.dump_network(net_index)
with open(pathname, 'w') as file:
json.dump(net_json, file, sort_keys=True, indent=4)
# Allow Save action, since we now know where to save to
self.last_save_path = pathname
self.save_item.Enable()
except IOError:
wx.LogError("Cannot save current data in file '{}'.".format(pathname))
[docs] def LoadFromJson(self):
with wx.FileDialog(self, "Load JSON file", wildcard="JSON files (*.json)|*.json",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# save the current contents in the file
pathname = fileDialog.GetPath()
try:
with open(pathname, 'r') as file:
net_json = json.load(file)
_net_index = self.controller.load_network(net_json)
except IOError:
wx.LogError("Cannot load network from file '{}'.".format(pathname))
[docs] def ManagePlugins(self, evt):
# TODO create special empty page that says "No plugins loaded"
with self.manager.create_dialog(self) as dlg:
dlg.Centre()
if dlg.ShowModal() == wx.ID_OK:
pass # exited normally
else:
pass # exited by clicking some button
[docs] def OverrideAccelTable(self, widget):
"""Set up functions to disable accelerator shortcuts for certain descendants of widgets.
This is to prevent accelerator shortcuts to be applied in unexpected situations, when
something other than the canvas is in foucs. For example, if the user is editing the name
of a node, they may use ctrl+Z to undo some text operation. However, since ctrl+Z is bound
to the "undo last operation" action on canvas, it will be caught by the canvas instead.
This prevents that by attaching a temporary, "null" accelerator table when a TextCtrl
widget goes into focus.
"""
if isinstance(widget, wx.TextCtrl):
def OnFocus(evt):
# for cb, item in self.menu_events:
# self.Unbind(wx.EVT_MENU, handler=cb, source=item)
# For some reason, we need to do this for both self and menubar to disable the
# AcceleratorTable. Don't ever lose this sacred knowledge, for it came at the cost
# of 50 minutes.
self.SetAcceleratorTable(wx.NullAcceleratorTable)
self.GetMenuBar().SetAcceleratorTable(wx.NullAcceleratorTable)
evt.Skip()
def OnUnfocus(evt):
# for cb, item in self.menu_events:
# self.Bind(wx.EVT_MENU, handler=cb, source=item)
self.SetAcceleratorTable(self.atable)
evt.Skip()
widget.Bind(wx.EVT_SET_FOCUS, OnFocus)
widget.Bind(wx.EVT_KILL_FOCUS, OnUnfocus)
for child in widget.GetChildren():
self.OverrideAccelTable(child)
[docs]class RKView(IView):
"""Implementation of the view class."""
def __init__(self):
self.controller = None
self.manager = None
self.app = None
[docs] def bind_controller(self, controller: IController):
self.controller = controller
[docs] def init(self):
assert self.controller is not None
self.app = wx.App()
self.frame = MainFrame(self.controller, title='RK Network Viewer')
init_api(self.frame.main_panel.canvas, self.controller)
self.canvas_panel = self.frame.main_panel.canvas
[docs] def main_loop(self):
assert self.app is not None
self.frame.Show()
self.app.MainLoop()
[docs] def update_all(self, nodes: List[Node], reactions: List[Reaction],
compartments: List[Compartment]):
"""Update the list of nodes.
Note that RKView takes ownership of the list of nodes and may modify it.
"""
self.canvas_panel.Reset(nodes, reactions, compartments)