@opencor/opencor 0.20250813.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +61 -0
- package/dist/index-BxzyBYAw.js +98574 -0
- package/dist/index.d.ts +8 -0
- package/dist/opencor.css +1 -0
- package/dist/opencor.es.js +5 -0
- package/dist/opencor.umd.js +6275 -0
- package/dist/quill-BGLY3Ud3.js +7521 -0
- package/package.json +90 -0
- package/src/App.vue +27 -0
- package/src/assets/app.css +14 -0
- package/src/assets/base.css +28 -0
- package/src/assets/logo.svg +17 -0
- package/src/common/common.ts +86 -0
- package/src/common/constants.ts +12 -0
- package/src/common/electron.ts +23 -0
- package/src/common/electronApi.ts +63 -0
- package/src/common/locCommon.ts +170 -0
- package/src/common/settings.ts +95 -0
- package/src/common/vueCommon.ts +50 -0
- package/src/components/BackgroundComponent.vue +26 -0
- package/src/components/ContentsComponent.vue +262 -0
- package/src/components/DragNDropComponent.vue +29 -0
- package/src/components/LoadOpencorComponent.vue +33 -0
- package/src/components/MainMenu.vue +220 -0
- package/src/components/OpenCOR.vue +546 -0
- package/src/components/SpinningWheelComponent.vue +15 -0
- package/src/components/dialogs/AboutDialog.vue +51 -0
- package/src/components/dialogs/BaseDialog.vue +11 -0
- package/src/components/dialogs/OpenRemoteDialog.vue +37 -0
- package/src/components/dialogs/ResetAllDialog.vue +13 -0
- package/src/components/dialogs/SettingsDialog.vue +42 -0
- package/src/components/dialogs/UpdateAvailableDialog.vue +16 -0
- package/src/components/dialogs/UpdateDownloadProgressDialog.vue +17 -0
- package/src/components/dialogs/UpdateErrorDialog.vue +18 -0
- package/src/components/dialogs/UpdateNotAvailableDialog.vue +12 -0
- package/src/components/propertyEditors/GraphsPropertyEditor.vue +3 -0
- package/src/components/propertyEditors/ParametersPropertyEditor.vue +3 -0
- package/src/components/propertyEditors/PropertyEditor.vue +61 -0
- package/src/components/propertyEditors/SimulationPropertyEditor.vue +45 -0
- package/src/components/propertyEditors/SolversPropertyEditor.vue +3 -0
- package/src/components/views/IssuesView.vue +37 -0
- package/src/components/views/SimulationExperimentUiView.vue +152 -0
- package/src/components/views/SimulationExperimentView.vue +214 -0
- package/src/components/widgets/GraphPanelWidget.vue +137 -0
- package/src/components/widgets/InputWidget.vue +128 -0
- package/src/libopencor/locApi.ts +167 -0
- package/src/libopencor/locFileApi.ts +263 -0
- package/src/libopencor/locLoggerApi.ts +36 -0
- package/src/libopencor/locSedApi.ts +486 -0
- package/src/libopencor/locUiJsonApi.ts +485 -0
- package/src/libopencor/locVersionApi.ts +17 -0
- package/src/libopencor/src/common.cpp +67 -0
- package/src/libopencor/src/common.h +27 -0
- package/src/libopencor/src/file.cpp +72 -0
- package/src/libopencor/src/file.h +15 -0
- package/src/libopencor/src/main.cpp +78 -0
- package/src/libopencor/src/sed.cpp +348 -0
- package/src/libopencor/src/sed.h +53 -0
- package/src/libopencor/src/version.cpp +8 -0
- package/src/libopencor/src/version.h +5 -0
- package/src/main.ts +5 -0
- package/src/types/types.d.ts +9 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as vue from 'vue'
|
|
2
|
+
|
|
3
|
+
// Some constants to know whether the operating system uses light mode or dark mode.
|
|
4
|
+
|
|
5
|
+
const prefersColorScheme = window.matchMedia('(prefers-color-scheme: light)')
|
|
6
|
+
|
|
7
|
+
export const isLightMode = vue.ref(prefersColorScheme.matches)
|
|
8
|
+
export const isDarkMode = vue.ref(!prefersColorScheme.matches)
|
|
9
|
+
|
|
10
|
+
prefersColorScheme.addEventListener('change', (event) => {
|
|
11
|
+
isLightMode.value = event.matches
|
|
12
|
+
isDarkMode.value = !event.matches
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// A method to track the height of a given element.
|
|
16
|
+
|
|
17
|
+
export function trackElementHeight(id: string): void {
|
|
18
|
+
vue.onMounted(() => {
|
|
19
|
+
const element = document.getElementById(id)
|
|
20
|
+
|
|
21
|
+
if (element !== null) {
|
|
22
|
+
const observer = new ResizeObserver(() => {
|
|
23
|
+
let elementHeight = window.getComputedStyle(element).height
|
|
24
|
+
|
|
25
|
+
if (elementHeight === '' || elementHeight === 'auto') {
|
|
26
|
+
elementHeight = '0px'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cssVariableName =
|
|
30
|
+
'--' +
|
|
31
|
+
id
|
|
32
|
+
.split('_')[0]
|
|
33
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
34
|
+
.toLowerCase() +
|
|
35
|
+
'-height'
|
|
36
|
+
const oldElementHeight = window.getComputedStyle(document.documentElement).getPropertyValue(cssVariableName)
|
|
37
|
+
|
|
38
|
+
if (oldElementHeight === '' || (elementHeight !== '0px' && oldElementHeight !== elementHeight)) {
|
|
39
|
+
document.documentElement.style.setProperty(cssVariableName, elementHeight)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
observer.observe(element)
|
|
44
|
+
|
|
45
|
+
vue.onUnmounted(() => {
|
|
46
|
+
observer.disconnect()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<img class="logo" src="../assets/logo.svg" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<style scoped>
|
|
6
|
+
.logo {
|
|
7
|
+
/* Original dimensions:
|
|
8
|
+
* - Width: 828.12px; and
|
|
9
|
+
* - Height: 704.92px.
|
|
10
|
+
* Final dimensions:
|
|
11
|
+
* - Width: 303px + 8px = 311px; and
|
|
12
|
+
* - Height: 257.92px + 8px = ~266px
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
max-width: 311px;
|
|
16
|
+
max-height: 266px;
|
|
17
|
+
border-radius: 1rem;
|
|
18
|
+
padding: 0.5rem;
|
|
19
|
+
box-shadow: 0 0 0.75rem 0.375rem var(--p-content-border-color);
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 50%;
|
|
22
|
+
left: 50%;
|
|
23
|
+
transform: translate(-50%, -50%);
|
|
24
|
+
z-index: -1;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="simulationOnly" class="h-full">
|
|
3
|
+
<div v-for="(fileTab, index) in fileTabs" :key="`tabPanel_${fileTab.file.path()}`" :value="fileTab.file.path()">
|
|
4
|
+
<IssuesView v-if="fileTab.file.issues().length !== 0" :issues="fileTab.file.issues()" />
|
|
5
|
+
<SimulationExperimentView
|
|
6
|
+
v-else-if="fileTab.uiJson === undefined"
|
|
7
|
+
:file="fileTabs[index].file"
|
|
8
|
+
:isActiveFile="fileTab.file.path() === activeFile"
|
|
9
|
+
:simulationOnly="true"
|
|
10
|
+
/>
|
|
11
|
+
<SimulationExperimentUiView
|
|
12
|
+
v-else
|
|
13
|
+
:file="fileTabs[index].file"
|
|
14
|
+
:simulationOnly="true"
|
|
15
|
+
:uiJson="fileTabs[index].uiJson"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div v-else class="h-full">
|
|
20
|
+
<BackgroundComponent v-show="fileTabs.length === 0" />
|
|
21
|
+
<Tabs
|
|
22
|
+
v-show="fileTabs.length !== 0"
|
|
23
|
+
id="fileTabs"
|
|
24
|
+
v-model:value="activeFile"
|
|
25
|
+
:scrollable="true"
|
|
26
|
+
:selectOnFocus="true"
|
|
27
|
+
>
|
|
28
|
+
<TabList id="fileTablist" class="file-tablist">
|
|
29
|
+
<Tab
|
|
30
|
+
v-for="fileTab in fileTabs"
|
|
31
|
+
:id="`tab_${fileTab.file.path()}`"
|
|
32
|
+
:key="`tab_${fileTab.file.path()}`"
|
|
33
|
+
:value="fileTab.file.path()"
|
|
34
|
+
>
|
|
35
|
+
<div class="flex gap-2 items-center">
|
|
36
|
+
<div>
|
|
37
|
+
{{
|
|
38
|
+
fileTab.file
|
|
39
|
+
.path()
|
|
40
|
+
.split(/(\\|\/)/g)
|
|
41
|
+
.pop()
|
|
42
|
+
}}
|
|
43
|
+
</div>
|
|
44
|
+
<div class="pi pi-times remove-button" @mousedown.prevent @click.stop="closeFile(fileTab.file.path())" />
|
|
45
|
+
</div>
|
|
46
|
+
</Tab>
|
|
47
|
+
</TabList>
|
|
48
|
+
<TabPanels class="p-0!">
|
|
49
|
+
<TabPanel
|
|
50
|
+
v-for="(fileTab, index) in fileTabs"
|
|
51
|
+
:key="`tabPanel_${fileTab.file.path()}`"
|
|
52
|
+
:value="fileTab.file.path()"
|
|
53
|
+
>
|
|
54
|
+
<IssuesView v-if="fileTab.file.issues().length !== 0" :issues="fileTab.file.issues()" />
|
|
55
|
+
<SimulationExperimentView
|
|
56
|
+
v-else-if="fileTab.uiJson === undefined"
|
|
57
|
+
:file="fileTabs[index].file"
|
|
58
|
+
:isActiveFile="fileTab.file.path() === activeFile"
|
|
59
|
+
/>
|
|
60
|
+
<SimulationExperimentUiView v-else :file="fileTabs[index].file" :uiJson="fileTabs[index].uiJson" />
|
|
61
|
+
</TabPanel>
|
|
62
|
+
</TabPanels>
|
|
63
|
+
</Tabs>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
import * as vueusecore from '@vueuse/core'
|
|
69
|
+
|
|
70
|
+
import * as vue from 'vue'
|
|
71
|
+
|
|
72
|
+
import * as common from '../common/common'
|
|
73
|
+
import { electronApi } from '../common/electronApi'
|
|
74
|
+
import * as vueCommon from '../common/vueCommon'
|
|
75
|
+
import * as locApi from '../libopencor/locApi'
|
|
76
|
+
|
|
77
|
+
export interface IFileTab {
|
|
78
|
+
file: locApi.File
|
|
79
|
+
uiJson?: locApi.IUiJson
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
defineProps<{
|
|
83
|
+
simulationOnly?: boolean
|
|
84
|
+
}>()
|
|
85
|
+
defineExpose({ openFile, closeCurrentFile, closeAllFiles, hasFile, hasFiles, selectFile })
|
|
86
|
+
|
|
87
|
+
export interface IContentsComponent {
|
|
88
|
+
openFile(file: locApi.File): void
|
|
89
|
+
closeCurrentFile(): void
|
|
90
|
+
closeAllFiles(): void
|
|
91
|
+
hasFile(filePath: string): boolean
|
|
92
|
+
hasFiles(): boolean
|
|
93
|
+
selectFile(filePath: string): void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fileTabs = vue.ref<IFileTab[]>([])
|
|
97
|
+
const activeFile = vue.ref<string>('')
|
|
98
|
+
|
|
99
|
+
const filePaths = vue.computed(() => {
|
|
100
|
+
const res: string[] = []
|
|
101
|
+
|
|
102
|
+
for (const fileTab of fileTabs.value) {
|
|
103
|
+
res.push(fileTab.file.path())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return res
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
vue.watch(filePaths, (filePaths) => {
|
|
110
|
+
electronApi?.filesOpened(filePaths)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
vue.watch(activeFile, (filePath) => {
|
|
114
|
+
// Note: activeFile can get updated by clicking on a tab or by calling selectFile(), hence we need to watch it to let
|
|
115
|
+
// people know that a file has been selected.
|
|
116
|
+
|
|
117
|
+
electronApi?.fileSelected(filePath)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function openFile(file: locApi.File): void {
|
|
121
|
+
const filePath = file.path()
|
|
122
|
+
const prevActiveFile = activeFile.value
|
|
123
|
+
|
|
124
|
+
selectFile(filePath)
|
|
125
|
+
|
|
126
|
+
fileTabs.value.splice(fileTabs.value.findIndex((fileTab) => fileTab.file.path() === prevActiveFile) + 1, 0, {
|
|
127
|
+
file: file,
|
|
128
|
+
uiJson: file.uiJson()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
electronApi?.fileOpened(filePath)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasFile(filePath: string): boolean {
|
|
135
|
+
return fileTabs.value.find((fileTab) => fileTab.file.path() === filePath) !== undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hasFiles(): boolean {
|
|
139
|
+
return fileTabs.value.length > 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function selectFile(filePath: string): void {
|
|
143
|
+
activeFile.value = filePath
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function selectNextFile(): void {
|
|
147
|
+
const activeFileIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value)
|
|
148
|
+
const nextFileIndex = (activeFileIndex + 1) % fileTabs.value.length
|
|
149
|
+
|
|
150
|
+
selectFile(fileTabs.value[nextFileIndex].file.path())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function selectPreviousFile(): void {
|
|
154
|
+
const activeFileIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value)
|
|
155
|
+
const nextFileIndex = (activeFileIndex - 1 + fileTabs.value.length) % fileTabs.value.length
|
|
156
|
+
|
|
157
|
+
selectFile(fileTabs.value[nextFileIndex].file.path())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function closeFile(filePath: string): void {
|
|
161
|
+
locApi.fileManager.unmanage(filePath)
|
|
162
|
+
|
|
163
|
+
const activeFileIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === filePath)
|
|
164
|
+
|
|
165
|
+
fileTabs.value.splice(activeFileIndex, 1)
|
|
166
|
+
|
|
167
|
+
if (activeFile.value === filePath && fileTabs.value.length > 0) {
|
|
168
|
+
selectFile(fileTabs.value[Math.min(activeFileIndex, fileTabs.value.length - 1)].file.path())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
electronApi?.fileClosed(filePath)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function closeCurrentFile(): void {
|
|
175
|
+
closeFile(activeFile.value)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function closeAllFiles(): void {
|
|
179
|
+
while (fileTabs.value.length > 0) {
|
|
180
|
+
closeCurrentFile()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Track the height of our file tablist.
|
|
185
|
+
|
|
186
|
+
vueCommon.trackElementHeight('fileTablist')
|
|
187
|
+
|
|
188
|
+
// Keyboard shortcuts.
|
|
189
|
+
|
|
190
|
+
if (!common.isMobile()) {
|
|
191
|
+
vueusecore.onKeyStroke((event: KeyboardEvent) => {
|
|
192
|
+
if (fileTabs.value.length === 0) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (event.ctrlKey && !event.shiftKey && event.code === 'Tab') {
|
|
197
|
+
event.preventDefault()
|
|
198
|
+
|
|
199
|
+
selectNextFile()
|
|
200
|
+
} else if (event.ctrlKey && event.shiftKey && event.code === 'Tab') {
|
|
201
|
+
event.preventDefault()
|
|
202
|
+
|
|
203
|
+
selectPreviousFile()
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
.file-tablist {
|
|
211
|
+
border-bottom: 1px solid var(--p-primary-color);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.p-tab {
|
|
215
|
+
padding: 0.25rem 0.5rem;
|
|
216
|
+
border-right: 1px solid var(--p-content-border-color);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.p-tab:first-of-type {
|
|
220
|
+
border-left: 1px solid var(--p-content-border-color);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.p-tab:hover {
|
|
224
|
+
background-color: var(--p-content-hover-background) !important;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.p-tab .remove-button {
|
|
228
|
+
visibility: hidden;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.p-tab:hover .remove-button,
|
|
232
|
+
.p-tab-active .remove-button {
|
|
233
|
+
visibility: visible;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.p-tab-active,
|
|
237
|
+
.p-tab-active:hover {
|
|
238
|
+
background-color: var(--p-primary-color) !important;
|
|
239
|
+
color: var(--p-primary-contrast-color);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
:deep(.p-tablist-active-bar) {
|
|
243
|
+
display: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.remove-button {
|
|
247
|
+
padding: 0.15rem;
|
|
248
|
+
font-size: 0.75rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.remove-button:hover {
|
|
252
|
+
border-radius: var(--p-border-radius-sm);
|
|
253
|
+
background-color: var(--p-red-500);
|
|
254
|
+
color: var(--p-red-50);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media (prefers-color-scheme: dark) {
|
|
258
|
+
.remove-button:hover {
|
|
259
|
+
background-color: var(--p-red-400);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="drop-area p-overlay-mask">
|
|
3
|
+
<div class="message">CellML files, SED-ML files, and COMBINE archives can be dropped here.</div>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<style scoped>
|
|
8
|
+
.drop-area {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
border: 0.375rem dashed;
|
|
12
|
+
border-color: var(--p-primary-color);
|
|
13
|
+
z-index: 99999;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.message {
|
|
17
|
+
font-size: 1.5rem;
|
|
18
|
+
text-align: center;
|
|
19
|
+
border-radius: 1rem;
|
|
20
|
+
padding: 0.5rem 1rem;
|
|
21
|
+
background-color: var(--p-primary-color);
|
|
22
|
+
color: var(--p-primary-contrast-color);
|
|
23
|
+
box-shadow: 0 0 0.75rem 0.375rem var(--p-content-border-color);
|
|
24
|
+
position: fixed;
|
|
25
|
+
top: 50%;
|
|
26
|
+
left: 50%;
|
|
27
|
+
transform: translate(-50%, -50%);
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="loading p-overlay-mask">
|
|
3
|
+
<Message class="message" severity="secondary">
|
|
4
|
+
<i class="message-icon pi pi-spin pi-cog" />
|
|
5
|
+
<br />
|
|
6
|
+
<span class="message-text">Loading OpenCOR...</span>
|
|
7
|
+
</Message>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<style scoped>
|
|
12
|
+
.loading {
|
|
13
|
+
z-index: 99999;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.message {
|
|
17
|
+
position: fixed;
|
|
18
|
+
top: 50%;
|
|
19
|
+
left: 50%;
|
|
20
|
+
transform: translate(-50%, -50%);
|
|
21
|
+
padding: 1.5rem 1rem 0.75rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.message-icon {
|
|
25
|
+
text-align: center;
|
|
26
|
+
display: block;
|
|
27
|
+
font-size: 3rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.message-text {
|
|
31
|
+
font-size: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Menubar id="mainMenu" :model="items">
|
|
3
|
+
<template #item="{ item, props }">
|
|
4
|
+
<a v-bind="props.action">
|
|
5
|
+
<div class="p-menubar-item-label">{{ item.label }}</div>
|
|
6
|
+
<svg
|
|
7
|
+
v-if="item.items !== undefined"
|
|
8
|
+
width="14"
|
|
9
|
+
height="14"
|
|
10
|
+
viewBox="0 0 14 14"
|
|
11
|
+
class="ml-auto p-icon p-menubar-submenu-icon"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M5.25 11.1728C5.14929 11.1694 5.05033 11.1455 4.9592 11.1025C4.86806 11.0595 4.78666 10.9984 4.72 10.9228C4.57955 10.7822 4.50066 10.5916 4.50066 10.3928C4.50066 10.1941 4.57955 10.0035 4.72 9.86283L7.72 6.86283L4.72 3.86283C4.66067 3.71882 4.64765 3.55991 4.68275 3.40816C4.71785 3.25642 4.79932 3.11936 4.91585 3.01602C5.03238 2.91268 5.17819 2.84819 5.33305 2.83149C5.4879 2.81479 5.64411 2.84671 5.78 2.92283L9.28 6.42283C9.42045 6.56346 9.49934 6.75408 9.49934 6.95283C9.49934 7.15158 9.42045 7.34221 9.28 7.48283L5.78 10.9228C5.71333 10.9984 5.63193 11.0595 5.5408 11.1025C5.44966 11.1455 5.35071 11.1694 5.25 11.1728Z"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
<div v-if="item.shortcut !== undefined" class="ml-auto border border-surface rounded bg-emphasis text-xs/3">
|
|
19
|
+
{{ item.shortcut }}
|
|
20
|
+
</div>
|
|
21
|
+
</a>
|
|
22
|
+
</template>
|
|
23
|
+
</Menubar>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import * as vueusecore from '@vueuse/core'
|
|
28
|
+
|
|
29
|
+
import * as vue from 'vue'
|
|
30
|
+
|
|
31
|
+
import * as common from '../common/common'
|
|
32
|
+
|
|
33
|
+
const props = defineProps<{
|
|
34
|
+
hasFiles: boolean
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits([
|
|
38
|
+
'about',
|
|
39
|
+
'close',
|
|
40
|
+
'closeAll',
|
|
41
|
+
'open',
|
|
42
|
+
'openRemote',
|
|
43
|
+
'openSampleLorenz',
|
|
44
|
+
'openSampleInteractiveLorenz',
|
|
45
|
+
'settings'
|
|
46
|
+
])
|
|
47
|
+
const isWindowsOrLinux = common.isWindows() || common.isLinux()
|
|
48
|
+
const isMacOs = common.isMacOs()
|
|
49
|
+
|
|
50
|
+
const items = [
|
|
51
|
+
{
|
|
52
|
+
label: 'File',
|
|
53
|
+
items: [
|
|
54
|
+
{
|
|
55
|
+
label: 'Open...',
|
|
56
|
+
shortcut: isWindowsOrLinux ? 'Ctrl+Alt+O' : isMacOs ? '⌘⌥O' : undefined,
|
|
57
|
+
command: () => {
|
|
58
|
+
emit('open')
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'Open Remote...',
|
|
63
|
+
shortcut: isWindowsOrLinux ? 'Ctrl+Shift+Alt+O' : isMacOs ? '⇧⌘⌥O' : undefined,
|
|
64
|
+
command: () => {
|
|
65
|
+
emit('openRemote')
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'Open Sample',
|
|
70
|
+
items: [
|
|
71
|
+
{
|
|
72
|
+
label: 'Lorenz',
|
|
73
|
+
command: () => {
|
|
74
|
+
emit('openSampleLorenz')
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: 'Interactive Lorenz',
|
|
79
|
+
command: () => {
|
|
80
|
+
emit('openSampleInteractiveLorenz')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{ separator: true },
|
|
86
|
+
{
|
|
87
|
+
label: 'Close',
|
|
88
|
+
shortcut: isWindowsOrLinux ? 'Ctrl+Alt+W' : isMacOs ? '⌘⌥W' : undefined,
|
|
89
|
+
command: () => {
|
|
90
|
+
emit('close')
|
|
91
|
+
},
|
|
92
|
+
disabled: () => !props.hasFiles
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Close All',
|
|
96
|
+
command: () => {
|
|
97
|
+
emit('closeAll')
|
|
98
|
+
},
|
|
99
|
+
disabled: () => !props.hasFiles
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
/*---OPENCOR--- Enable the settings menu once we have settings for OpenCOR's Web app.
|
|
104
|
+
{
|
|
105
|
+
label: 'Tools',
|
|
106
|
+
items: [
|
|
107
|
+
{
|
|
108
|
+
label: 'Settings...',
|
|
109
|
+
shortcut: isWindowsOrLinux ? 'Ctrl+Alt+,' : isMacOs ? '⌘⌥,' : undefined,
|
|
110
|
+
command: () => {
|
|
111
|
+
emit('settings')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
*/
|
|
117
|
+
{
|
|
118
|
+
label: 'Help',
|
|
119
|
+
items: [
|
|
120
|
+
{
|
|
121
|
+
label: 'Home Page',
|
|
122
|
+
command: () => {
|
|
123
|
+
window.open('https://opencor.ws/')
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{ separator: true },
|
|
127
|
+
{
|
|
128
|
+
label: 'Report Issue',
|
|
129
|
+
command: () => {
|
|
130
|
+
window.open('https://github.com/opencor/webapp/issues/new')
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{ separator: true },
|
|
134
|
+
{
|
|
135
|
+
label: 'About OpenCOR',
|
|
136
|
+
command: () => {
|
|
137
|
+
emit('about')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
// Never display our menu as a hamburger menu.
|
|
145
|
+
|
|
146
|
+
vue.onMounted(() => {
|
|
147
|
+
const mainMenu = document.getElementById('mainMenu')
|
|
148
|
+
|
|
149
|
+
if (mainMenu !== null) {
|
|
150
|
+
const observer = new MutationObserver(() => {
|
|
151
|
+
if (mainMenu.className.includes('p-menubar-mobile')) {
|
|
152
|
+
mainMenu.classList.remove('p-menubar-mobile')
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
observer.observe(mainMenu, { attributes: true })
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Keyboard shortcuts.
|
|
161
|
+
|
|
162
|
+
if (!common.isMobile()) {
|
|
163
|
+
vueusecore.onKeyStroke((event: KeyboardEvent) => {
|
|
164
|
+
if (common.isCtrlOrCmd(event) && !event.shiftKey && event.code === 'KeyO') {
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
|
|
167
|
+
emit('open')
|
|
168
|
+
} else if (common.isCtrlOrCmd(event) && event.shiftKey && event.code === 'KeyO') {
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
|
|
171
|
+
emit('openRemote')
|
|
172
|
+
} else if (props.hasFiles && common.isCtrlOrCmd(event) && !event.shiftKey && event.code === 'KeyW') {
|
|
173
|
+
event.preventDefault()
|
|
174
|
+
|
|
175
|
+
emit('close')
|
|
176
|
+
} else if (common.isCtrlOrCmd(event) && !event.shiftKey && event.code === 'Comma') {
|
|
177
|
+
event.preventDefault()
|
|
178
|
+
|
|
179
|
+
emit('settings')
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<style scoped>
|
|
186
|
+
.p-menubar {
|
|
187
|
+
padding: 0.1rem;
|
|
188
|
+
border: none;
|
|
189
|
+
border-radius: 0;
|
|
190
|
+
border-bottom: 1px solid var(--border-color);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.p-menubar
|
|
194
|
+
> .p-menubar-root-list
|
|
195
|
+
> .p-menubar-item
|
|
196
|
+
> .p-menubar-item-content
|
|
197
|
+
> .p-menubar-item-link
|
|
198
|
+
.p-menubar-submenu-icon {
|
|
199
|
+
display: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.p-menubar-item-link {
|
|
203
|
+
padding: 0.25rem 0.5rem !important;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
:deep(.p-menubar-root-list) {
|
|
207
|
+
gap: 0.1rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
:deep(.p-menubar-submenu) {
|
|
211
|
+
padding: 0.1rem;
|
|
212
|
+
z-index: 10;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.shortcut {
|
|
216
|
+
border-color: var(--p-content-border-color);
|
|
217
|
+
background: var(--p-content-hover-background);
|
|
218
|
+
color: var(--p-text-muted-color);
|
|
219
|
+
}
|
|
220
|
+
</style>
|